CVE-2023-39413,CVE-2023-39414
Multiple integer underflow vulnerabilities exist in the LXT2 lxt2_rd_iter_radix shift operation functionality of GTKWave 3.3.115. A specially crafted .lxt2 file can lead to memory corruption. A victim would need to open a malicious file to trigger these vulnerabilities.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
GTKWave 3.3.115
GTKWave - https://gtkwave.sourceforge.net
7.0 - CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-191 - Integer Underflow (Wrap or Wraparound)
GTKWave is a wave viewer, often used to analyze FPGA simulations and logic analyzer captures. It includes a GUI to view and analyze traces, as well as convert across several file formats (.lxt
, .lxt2
, .vzt
, .fst
, .ghw
, .vcd
, .evcd
) either by using the UI or its command line tools. GTKWave is available for Linux, Windows and MacOS. Trace files can be shared within teams or organizations, for example to compare results of simulation runs across different design implementations, to analyze protocols captured with logic analyzers or just as a reference when porting design implementations.
GTKWave sets up mime types for its supported extensions. So, for example, it’s enough for a victim to double-click on a wave file received by e-mail to cause the gtkwave program to be executed and load a potentially malicious file.
LXT2 (InterLaced eXtensible Trace Version 2) files are parsed by the functions found in lxt2_read.c
. These functions are used in the lxt2vcd
file conversion utility, rtlbrowse
, lxt2miner
, and by the GUI portion of GTKwave, which are thus all affected by the issue described in this report.
To parse LXT2 files, the function lxt2_rd_init
is called:
struct lxt2_rd_trace *lxt2_rd_init(const char *name) {
[1] struct lxt2_rd_trace *lt = (struct lxt2_rd_trace *)calloc(1, sizeof(struct lxt2_rd_trace));
lxtint32_t i;
[2] if (!(lt->handle = fopen(name, "rb"))) {
lxt2_rd_close(lt);
lt = NULL;
} else {
lxtint16_t id = 0, version = 0;
...
[3] if (!fread(&id, 2, 1, lt->handle)) {
id = 0;
}
if (!fread(&version, 2, 1, lt->handle)) {
id = 0;
}
if (!fread(<->granule_size, 1, 1, lt->handle)) {
id = 0;
}
At [1] the lt
structure is initialized. This is the structure that will contain all the information about the input file.
The input file is opened [2] and 3 fields are read [3] to make sure the input file is a supported LXT2 file.
...
[4] rcf = fread(<->numfacbytes, 4, 1, lt->handle);
lt->numfacbytes = rcf ? lxt2_rd_get_32(<->numfacbytes, 0) : 0;
rcf = fread(<->longestname, 4, 1, lt->handle);
lt->longestname = rcf ? lxt2_rd_get_32(<->longestname, 0) : 0;
rcf = fread(<->zfacnamesize, 4, 1, lt->handle);
lt->zfacnamesize = rcf ? lxt2_rd_get_32(<->zfacnamesize, 0) : 0;
rcf = fread(<->zfacname_predec_size, 4, 1, lt->handle);
lt->zfacname_predec_size = rcf ? lxt2_rd_get_32(<->zfacname_predec_size, 0) : 0;
rcf = fread(<->zfacgeometrysize, 4, 1, lt->handle);
lt->zfacgeometrysize = rcf ? lxt2_rd_get_32(<->zfacgeometrysize, 0) : 0;
rcf = fread(<->timescale, 1, 1, lt->handle);
if (!rcf) lt->timescale = 0; /* no swap necessary */
...
Several fields are then read from the file [4]:
numfacs
: the number of facilities (elements in facnames
)numfacbytes
: unusedlongestname
: keeps the longest length of all defined facilities’ nameszfacnamesize
: compressed size of facnames
zfacname_predec_size
: decompressed size of facnames
zfacgeometrysize
: compressed size of facgeometry
For this advisory, we’re interested in the extraction and parsing of facgeometry
:
fseeko(lt->handle, pos = pos + lt->zfacnamesize, SEEK_SET);
/* fprintf(stderr, LXT2_RDLOAD"seeking to geometry at %d (0x%08x)\n", pos, pos); */
lt->zhandle = gzdopen(dup(fileno(lt->handle)), "rb");
[5] t = lt->numfacs * 4 * sizeof(lxtint32_t);
[6] m = (char *)malloc(t);
[7] rc = gzread(lt->zhandle, m, t);
gzclose(lt->zhandle);
lt->zhandle = NULL;
if (rc != t) {
fprintf(stderr, LXT2_RDLOAD "*** geometry section mangled %d (act) vs %d (exp)\n", rc, t);
...
}
The decompressed facgeometry
structure is pointed to by m
[6] and decompressed at [7] using gzread
. The expected decompressed size is expected to be numfacs * 16
[5].
Each geometry has 4 fields associated: rows
, msb
, lsb
, flags
. These are extracted in the following code, together with len
and value
arrays, which are calculated based on the 4 fields just mentioned.
...
for (i = 0; i < lt->numfacs; i++) {
lt->rows[i] = lxt2_rd_get_32(m + i * 16, 0);
[8] lt->msb[i] = lxt2_rd_get_32(m + i * 16, 4);
lt->lsb[i] = lxt2_rd_get_32(m + i * 16, 8);
lt->flags[i] = lxt2_rd_get_32(m + i * 16, 12);
if (!(lt->flags[i] & LXT2_RD_SYM_F_INTEGER)) {
[9] lt->len[i] = (lt->msb[i] <= lt->lsb[i]) ? (lt->lsb[i] - lt->msb[i] + 1) : (lt->msb[i] - lt->lsb[i] + 1);
} else {
lt->len[i] = 32;
}
[10] lt->value[i] = calloc(lt->len[i] + 1, sizeof(char));
}
...
In the code above, several fields are extracted from m
(the decompressed facgeometry
). In particular, msb
and lsb
[8] are used to fill a len
array, which is used to keep the signed difference between msb
and lsb
[9].
Additionally, the lt->value[i]
element is filled with a pointer to a buffer that has its size calculated as lt->len[i] + 1
[10].
At this point it’s important to note that we can set lt->len[i]
to an arbitrary value. In particular, we’re interested in setting it to 0. This is possible by setting msb
to 0x80000000
and lsb
to 0x7fffffff
or vice versa. This will lead to allocating an lt->value[i]
array of 1 element (1 byte) at [10].
Upon return from the current lxt2_rd_init
function, the blocks are parsed inside lxt2_rd_iter_blocks
by walking the linked list created at [7].
int lxt2_rd_iter_blocks(struct lxt2_rd_trace *lt,
void (*value_change_callback)(struct lxt2_rd_trace **lt, lxtint64_t *time, lxtint32_t *facidx, char **value),
void *user_callback_data_pointer) {
struct lxt2_rd_block *b;
int blk = 0, blkfinal = 0;
int processed = 0;
struct lxt2_rd_block *bcutoff = NULL, *bfinal = NULL;
int striped_kill = 0;
unsigned int real_uncompressed_siz = 0;
unsigned char gzid[2];
lxtint32_t i;
...
b = lt->block_head;
blk = 0;
...
while (b) {
...
fseeko(lt->handle, b->filepos, SEEK_SET);
gzid[0] = gzid[1] = 0;
if (!fread(&gzid, 2, 1, lt->handle)) {
gzid[0] = gzid[1] = 0;
}
fseeko(lt->handle, b->filepos, SEEK_SET);
[11] if ((striped_kill = (gzid[0] != 0x1f) || (gzid[1] != 0x8b))) {
...
} else {
int rc;
b->mem = malloc(b->uncompressed_siz);
[12] lt->zhandle = gzdopen(dup(fileno(lt->handle)), "rb");
rc = gzread(lt->zhandle, b->mem, b->uncompressed_siz);
gzclose(lt->zhandle);
lt->zhandle = NULL;
[13] if (((lxtint32_t)rc) != b->uncompressed_siz) {
fprintf(stderr, LXT2_RDLOAD "short read on block %d vs " LXT2_RD_LD " (exp), ignoring\n", rc, b->uncompressed_siz);
free(b->mem);
b->mem = NULL;
b->short_read_ignore = 1;
} else {
lt->block_mem_consumed += b->uncompressed_siz;
}
}
bfinal = b;
blkfinal = blk;
}
if (b->mem) {
[14] lxt2_rd_process_block(lt, b);
...
}
blk++;
b = b->next;
}
If the block starts with the gzip magic [11], gzdopen
is used to decompress the block [12], and, if the decompression is successful [13], lxt2_rd_process_block()
is called to parse the decompressed block contents [14] (which are pointed to by b->mem
).
lxt2_rd_process_block()
parses the whole block structure, extracting several fields. Eventually, it calls lxt2_rd_iter_radix()
:
void lxt2_rd_iter_radix(struct lxt2_rd_trace *lt, struct lxt2_rd_block *b) {
unsigned int which_time;
int offset;
void **top_elem;
granmsk_t msk = ~LXT2_RD_GRAN_1VAL;
lxtint32_t x;
for (which_time = 0; which_time < lt->num_time_table_entries; which_time++, msk <<= 1)
while ((top_elem = lt->radix_sort[which_time])) {
lxtint32_t idx = top_elem - lt->next_radix;
[15] switch (lt->fac_curpos_width) {
case 1:
vch = lxt2_rd_get_byte(lt->fac_curpos[idx], 0);
break;
case 2:
vch = lxt2_rd_get_16(lt->fac_curpos[idx], 0);
break;
case 3:
vch = lxt2_rd_get_24(lt->fac_curpos[idx], 0);
break;
case 4:
default:
vch = lxt2_rd_get_32(lt->fac_curpos[idx], 0);
break;
}
...
[16] switch (vch) {
...
case LXT2_RD_ENC_LSH0:
[17] case LXT2_RD_ENC_LSH1:
memmove(lt->value[idx], lt->value[idx] + 1, lt->len[idx] - 1);
lt->value[idx][lt->len[idx] - 1] = '0' + (vch - LXT2_RD_ENC_LSH0);
break;
case LXT2_RD_ENC_RSH0:
[18] case LXT2_RD_ENC_RSH1:
memmove(lt->value[idx] + 1, lt->value[idx], lt->len[idx] - 1);
lt->value[idx][0] = '0' + (vch - LXT2_RD_ENC_RSH0);
break;
...
At [15], values from lt->fac_curpos
are extracted directly from the uncompressed portion of the block, so vch
can be arbitrarily controlled.
Depending on vch
[16], one of the many operations (here truncated for brevity) are used to modify the lt->value[idx]
array.
If vch
is LXT2_RD_ENC_LSH0
or LXT2_RD_ENC_LSH1
[17], memmove
is used to move the values within lt->value[idx]
to the left by 1 byte (left shift operation).
However, if lt->len[idx]
is 0, lt->len[idx] - 1
will underflow, leading to calling memmove
with a size of -1. This will lead to a large copy operation that will corrupt memory. In the same way, the right shift operation case at [18] can lead to a large copy that will corrupt memory.
Because of the multi-threaded nature of GTKWave, an attacker may be able to exploit this issue to execute arbitrary code.
When vch
is either LXT2_RD_ENC_LSH0
or LXT2_RD_ENC_LSH1
[17] and lt->len[idx]
is 0, the operation lt->len[idx] - 1
will underflow, leading to calling memmove
with a size of -1. This will lead to a large copy operation that will corrupt memory.
When vch
is either LXT2_RD_ENC_RSH0
or LXT2_RD_ENC_RSH1
[18] and lt->len[idx]
is 0, the operation lt->len[idx] - 1
will underflow, leading to calling memmove
with a size of -1. This will lead to a large copy operation that will corrupt memory.
==862802==ERROR: AddressSanitizer: negative-size-param: (size=-1)
#0 0xf79debd6 in __interceptor_memmove ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:882
#1 0x5655910e in lxt2_rd_iter_radix src/helpers/lxt2_read.c:222
#2 0x5655fec3 in lxt2_rd_process_block src/helpers/lxt2_read.c:729
#3 0x565697ca in lxt2_rd_iter_blocks src/helpers/lxt2_read.c:1604
#4 0x5656b5b1 in process_lxt src/helpers/lxt2vcd.c:299
#5 0x5656bee6 in main src/helpers/lxt2vcd.c:458
#6 0xf763b7c4 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#7 0xf763b887 in __libc_start_main_impl ../csu/libc-start.c:360
#8 0x565573a6 in _start (lxt2vcd+0x23a6) (BuildId: dda13598941c0925ee73e101a327141c183f0c04)
0xf4e00510 is located 0 bytes inside of 1-byte region [0xf4e00510,0xf4e00511)
allocated by thread T0 here:
#0 0xf7a5d4fb in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:77
#1 0x56562a37 in lxt2_rd_init src/helpers/lxt2_read.c:925
#2 0x5656aec9 in process_lxt src/helpers/lxt2vcd.c:183
#3 0x5656bee6 in main src/helpers/lxt2vcd.c:458
#4 0xf763b7c4 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: negative-size-param ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:882 in __interceptor_memmove
AddressSanitizer: CHECK failed: asan_poisoning.cpp:185 "((beg)) < ((end))" (0xf4e00510, 0xf4e0050f) (tid=862802)
#0 0xf7a69cb2 in CheckUnwind ../../../../src/libsanitizer/asan/asan_rtl.cpp:69
#1 0xf7a9347f in __sanitizer::CheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_termination.cpp:86
#2 0xf7a617a0 in __asan_region_is_poisoned ../../../../src/libsanitizer/asan/asan_poisoning.cpp:185
#3 0xf79de812 in __interceptor_memmove ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:882
#4 0x5655910e in lxt2_rd_iter_radix src/helpers/lxt2_read.c:222
#5 0x5655fec3 in lxt2_rd_process_block src/helpers/lxt2_read.c:729
#6 0x565697ca in lxt2_rd_iter_blocks src/helpers/lxt2_read.c:1604
#7 0x5656b5b1 in process_lxt src/helpers/lxt2vcd.c:299
#8 0x5656bee6 in main src/helpers/lxt2vcd.c:458
#9 0xf763b7c4 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#10 0xf763b887 in __libc_start_main_impl ../csu/libc-start.c:360
#11 0x565573a6 in _start (lxt2vcd+0x23a6) (BuildId: dda13598941c0925ee73e101a327141c183f0c04)
Fixed in version 3.3.118, available from https://sourceforge.net/projects/gtkwave/files/gtkwave-3.3.118/
2023-08-11 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.