CVE-2025-23338
An improper array index validation vulnerability exists in the symbol table parsing functionality of NVIDIA nvdisasm 12.8.90. A specially crafted ELF file can lead to an out-of-bounds write. An attacker can provide a malicious file 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.
NVIDIA nvdisasm 12.8.90
nvdisasm - https://docs.nvidia.com/cuda/cuda-binary-utilities/index.html
7.8 - CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-129 - Improper Validation of Array Index
The nvdisasm
tool provided by the Nvidia CUDA Toolkit is used to display information about CUDA ELF files. Apart from the disassembly of CUDA compiled binary code, it is capable of displaying control flow graphs, register life-ranges, debug information etc.
As per the standard ELF specification, an ELF file can include symbol information in the .symtab
section. Each entry in the symbol table has the following layout:
typedef struct {
Elf64_Word st_name;
unsigned char st_info;
unsigned char st_other;
Elf64_Half st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_size;
} Elf64_Sym;
It is possible to see the symbol table entries of an ELF file with the standard readelf
utility:
$ readelf -s ./elf
Symbol table '.symtab' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000efffffff 1000 SECTION LOCAL DEFAULT 9 .text._Z7add
2: ffffffed00000000 1000 SECTION LOCAL DEFAULT 10 .nv.shared._
3: 0000004e00000005 20 SECTION LOCAL DEFAULT 8 .nv.constant0
In the function at 0x418af0
, nvdisasm
parses .symtab
entries. As parameters it takes the st_value
, the st_index
of the entry and another internal structure of the application.
Depending on the st_value
and the st_size
fields, the code selects a a constant for a shift operation. For example, if at (1) below the rcx
and r9
are equal after some math operations, it proceeds to put the 0x2c
constant at rsp+0x8
at (2).
00418be7 mov r8, 0xfffffffffff
00418bf1 mov r9, rdx
00418bf4 mov r13, rdi
00418bf7 and r9, r8
00418bfa and r13, r8
00418bfd sub r9, r13
00418c00 cmp rcx, r9 (1)
00418c03 jne 0x4199b7
...
004199b7 mov dword [rsp+0x8 {var_80_1}], 0x2c (2)
004199bf jmp 0x4196e0
The shift constant can range from 0x0
to 0x3c
in increments of 4
. Then, continuing at location 0x04196e0
we see:
004196e0 mov edi, dword [rsp+0x8 {var_80_1}] (3)
...
004196f9 shr rdx, cl (4)
00419700 mov qword [rsp+0x10 {var_78_1}], rdx (5)
Here rdx
has the st_value
of the entry. This value is being shifted to the right at (4) by the shift constant chosen earlier, retrieved at (3). Then this new value is saved at rsp+0x10
at (5).
Continuing, execution reaches 0x0418d46
where the shifted value is retrieved and then an AND
operation with 0xf
takes place. Note here, that at (6), an 8-byte value is retrieved but after the AND
, the lower 4 bytes are all set to 0
, except from the last one where it will have a maximum value of 0xf
.
00418d46 mov rbp, qword [rsp+0x10 {var_78_1}] (6)
00418d4b and ebp, 0xf (7)
At 0x041954d
, rbp
is incremented by 1
, setting the maximum value of rbp
here to 0x10
.
0041954d add rbp, 0x1
Finally, we see the following code:
00418d87 mov rdi, qword [rbx+rbp*8+0x20] (8)
00418d8c test rdi, rdi
00418d8f je 0x418d96
00418d91 call sub_4178e0 (9)
00418d96 sub r12, r13
00418d99 mov qword [rbx+rbp*8+0x20], r15 (10)
At (8), we see the shifted value being used as an index to the buffer at rbx
, adding an offset of 0x20
. The maximum value of the expression with rbp = 0x10
will be
rbx + 0x10*8 + 0x20 = rbx + 0xa0
The buffer has a size of 0xa0
, which is passed at the high level allocation function at 0x40a610
:
004191ea mov esi, 0xa0
004191ef call sub_40a610
As a result, the instruction at (8) will retrieve an 8-byte pointer from the offset rbx+0xa0
, which will read 8 bytes beyond the end of the buffer. If the pointer retrieved is NULL
, execution will continue at the function call at (9). However, even if the pointer is not NULL
, execution will reach the instruction at (10), where the pointer at the register r15
(that points to an offset in the file input) will be written to those 8 bytes beyond the allocated buffer.
As a result, an attacker can write a pointer to attacker controlled data, 8 bytes beyond an allocated buffer, which could lead to code execution.
Output from Valgrind shows:
==10666== Memcheck, a memory error detector
==10666== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==10666== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==10666== Command: ./nvdisasm-12.8.90 poc
==10666==
==10666== Invalid read of size 8
==10666== at 0x418D87: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45BEC3: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45CEA5: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x40307A: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x48A21C9: (below main) (libc_start_call_main.h:58)
==10666== Address 0x4a9cfe8 is 0 bytes after a block of size 168 alloc'd
==10666== at 0x484680F: malloc (vg_replace_malloc.c:446)
==10666== by 0x465719: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x40A999: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x4191F3: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45BEC3: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45CEA5: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x40307A: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x48A21C9: (below main) (libc_start_call_main.h:58)
==10666==
==10666== Invalid write of size 8
==10666== at 0x418D99: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45BEC3: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45CEA5: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x40307A: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x48A21C9: (below main) (libc_start_call_main.h:58)
==10666== Address 0x4a9cfe8 is 0 bytes after a block of size 168 alloc'd
==10666== at 0x484680F: malloc (vg_replace_malloc.c:446)
==10666== by 0x465719: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x40A999: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x4191F3: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45BEC3: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45CEA5: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x40307A: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x48A21C9: (below main) (libc_start_call_main.h:58)
==10666==
==10666== Invalid read of size 1
==10666== at 0x417913: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x418D95: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45BEC3: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45CEA5: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x40307A: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x48A21C9: (below main) (libc_start_call_main.h:58)
==10666== Address 0xb4 is not stack'd, malloc'd or (recently) free'd
==10666==
==10666==
==10666== Process terminating with default action of signal 11 (SIGSEGV)
==10666== Access not within mapped region at address 0xB4
==10666== at 0x417913: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x418D95: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45BEC3: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x45CEA5: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x40307A: ??? (in /home/dtatsis/nvdisasm/trizzle-new/analysis/0x412B79-write8-d2d20018-off-by-one-and-more/nvdisasm-12.8.90)
==10666== by 0x48A21C9: (below main) (libc_start_call_main.h:58)
2025-04-02 - Vendor Disclosure
2025-09-23 - Vendor Patch Release
2025-09-24 - Public Release
Discovered by Dimitrios Tatsis of Cisco Talos.