CVE-2026-22879
A heap-based buffer overflow vulnerability exists in the vtkDICOMItem::FindDataElementOrInsert functionality of vtk-dicom (version(s): 9.5.2). A specially crafted DICOM file can lead to heap-based memory corruption. 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.
vtk-dicom (version(s): 9.5.2)
vtk-dicom - https://dgobbi.github.io/vtk-dicom/
8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-129 - Improper Validation of Array Index
A VTK-DICOM solution is a technology stack or library that enables reading, interpreting, and visualizing DICOM (Digital Imaging and Communications in Medicine) medical imaging data using VTK (Visualization Toolkit). It combines DICOM input/output with VTK’s processing and rendering capabilities so that applications can handle clinical image formats like CT and MRI and display them interactively.
A specially crafted DICOM file can trigger an out-of-bounds write in the vtkDICOMItem::FindDataElementOrInsert function. The vulnerability occurs due to an increment of a value corresponding to an array index that exceeds the size of the array itself, introduces a corruption of heap chunk metadata.
By processing the malicious file, we reach the following situation:
corrupted size vs. prev_size
Program received signal SIGABRT, Aborted.
__pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
⚠️ warning: 44 ./nptl/pthread_kill.c: No such file or directory
To gain a deeper understanding of the internal execution flow, we need to analyze the call stack.
#0 __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
#1 __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2 __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3 0x000073583fb4527e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4 0x000073583fb288ff in __GI_abort () at ./stdlib/abort.c:79
#5 0x000073583fb297b6 in __libc_message_impl (fmt=fmt@entry=0x73583fcce8d7 "%s\n") at ../sysdeps/posix/libc_fatal.c:134
#6 0x000073583fba8ff5 in malloc_printerr (str=str@entry=0x73583fccc5e8 "corrupted size vs. prev_size") at ./malloc/malloc.c:5772
#7 0x000073583fba9b96 in unlink_chunk (p=p@entry=0x57d94aa2e610, av=0x73583fd03ac0 <main_arena>) at ./malloc/malloc.c:1611
#8 0x000073583fbab0e3 in _int_free_create_chunk (nextsize=48, nextchunk=0x57d94aa2e610, size=<optimized out>, p=0x57d94aa2e580, av=0x73583fd03ac0 <main_arena>) at ./malloc/malloc.c:4721
#9 _int_free_merge_chunk (av=0x73583fd03ac0 <main_arena>, p=0x57d94aa2e580, size=<optimized out>) at ./malloc/malloc.c:4700
#10 0x000073583fbaddae in __GI___libc_free (mem=0x57d94aa2e590) at ./malloc/malloc.c:3398
#11 0x0000735844b2cf02 in vtkDICOMItem::FreeList (this=0x57d94aa31990) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMItem.cxx:105
#12 0x0000735844a9826d in vtkDICOMItem::Clear (this=0x57d94aa31990) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMItem.h:94
#13 0x0000735844a9821a in vtkDICOMItem::~vtkDICOMItem (this=0x57d94aa31990, __in_chrg=<optimized out>) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMItem.h:88
#14 0x0000735844b33f51 in vtkDICOMValue::FreeValue (v=0x57d94aa31980) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMValue.cxx:1204
#15 0x0000735844a97c2e in vtkDICOMValue::Clear (this=0x57d94aa30ae0) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMValue.h:168
#16 0x0000735844a97bdc in vtkDICOMValue::~vtkDICOMValue (this=0x57d94aa30ae0, __in_chrg=<optimized out>) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMValue.h:148
#17 0x0000735844a981fa in vtkDICOMDataElement::~vtkDICOMDataElement (this=0x57d94aa30ad8, __in_chrg=<optimized out>) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMDataElement.h:29
#18 0x0000735844a916f7 in vtkDICOMMetaData::Clear (this=0x57d94aa27540) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMMetaData.cxx:71
#19 0x0000735844a9157a in vtkDICOMMetaData::~vtkDICOMMetaData (this=0x57d94aa27540, __in_chrg=<optimized out>) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMMetaData.cxx:51
#20 0x0000735844a91632 in vtkDICOMMetaData::~vtkDICOMMetaData (this=0x57d94aa27540, __in_chrg=<optimized out>) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMMetaData.cxx:60
#21 0x0000735843255771 in vtkObjectBase::UnRegisterInternal (this=0x57d94aa27540, check=0) at /work/vtk/source/Common/Core/vtkObjectBase.cxx:334
#22 0x000073584324f6e5 in vtkObject::UnRegisterInternal (this=0x57d94aa27540, o=0x0, check=0) at /work/vtk/source/Common/Core/vtkObject.cxx:946
#23 0x0000735843255611 in vtkObjectBase::UnRegister (this=0x57d94aa27540, o=0x0) at /work/vtk/source/Common/Core/vtkObjectBase.cxx:289
#24 0x000073584325526e in vtkObjectBase::Delete (this=0x57d94aa27540) at /work/vtk/source/Common/Core/vtkObjectBase.cxx:228
#25 0x0000735844b06e7e in vtkDICOMReader::~vtkDICOMReader (this=0x57d94aa24d90, __in_chrg=<optimized out>) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMReader.cxx:164
#26 0x0000735844b06f14 in vtkDICOMReader::~vtkDICOMReader (this=0x57d94aa24d90, __in_chrg=<optimized out>) at /work/vtk/source/Remote/vtkDICOM/Source/vtkDICOMReader.cxx:174
#27 0x0000735843255771 in vtkObjectBase::UnRegisterInternal (this=0x57d94aa24d90, check=0) at /work/vtk/source/Common/Core/vtkObjectBase.cxx:334
#28 0x000073584324f6e5 in vtkObject::UnRegisterInternal (this=0x57d94aa24d90, o=0x7ffdd75cf510, check=0) at /work/vtk/source/Common/Core/vtkObject.cxx:946
#29 0x000073584319590b in vtkGarbageCollectorToObjectBaseFriendship::UnRegister (obj=0x57d94aa24d90, from=0x7ffdd75cf510) at /work/vtk/source/Common/Core/vtkGarbageCollector.cxx:129
#30 0x00007358431937e0 in vtkGarbageCollectorImpl::CollectComponent (this=0x7ffdd75cf510, c=0x57d94aa36a20) at /work/vtk/source/Common/Core/vtkGarbageCollector.cxx:676
#31 0x00007358431926ee in vtkGarbageCollectorImpl::CollectInternal (this=0x7ffdd75cf510, root=0x57d94aa24d90) at /work/vtk/source/Common/Core/vtkGarbageCollector.cxx:454
#32 0x0000735843194dcf in vtkGarbageCollector::Collect (root=0x57d94aa24d90) at /work/vtk/source/Common/Core/vtkGarbageCollector.cxx:873
#33 0x0000735843255785 in vtkObjectBase::UnRegisterInternal (this=0x57d94aa24d90, check=1) at /work/vtk/source/Common/Core/vtkObjectBase.cxx:342
#34 0x000073584324f6e5 in vtkObject::UnRegisterInternal (this=0x57d94aa24d90, o=0x0, check=1) at /work/vtk/source/Common/Core/vtkObject.cxx:946
#35 0x0000735843255611 in vtkObjectBase::UnRegister (this=0x57d94aa24d90, o=0x0) at /work/vtk/source/Common/Core/vtkObjectBase.cxx:289
#36 0x00007358432ae23f in vtkSmartPointerBase::~vtkSmartPointerBase (this=0x7ffdd75cf860, __in_chrg=<optimized out>) at /work/vtk/source/Common/Core/vtkSmartPointerBase.cxx:49
#37 0x000057d919724f88 in vtkSmartPointer<vtkDICOMReader>::~vtkSmartPointer (this=0x7ffdd75cf860, __in_chrg=<optimized out>) at /usr/local/include/vtk-9.5/vtkSmartPointer.h:25
#38 0x000057d9197246b5 in process_one_file (inputPath=..., outputPath=...) at /work/harness/dicom2vtk.cpp:28
#39 0x000057d919724c4e in main (argc=5, argv=0x7ffdd75cfa78) at /work/harness/dicom2vtk.cpp:101
#40 0x000073583fb2a1ca in __libc_start_call_main (main=main@entry=0x57d919724851 <main(int, char**)>, argc=argc@entry=5, argv=argv@entry=0x7ffdd75cfa78)
at ../sysdeps/nptl/libc_start_call_main.h:58
#41 0x000073583fb2a28b in __libc_start_main_impl (main=0x57d919724851 <main(int, char**)>, argc=5, argv=0x7ffdd75cfa78, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=0x7ffdd75cfa68) at ../csu/libc-start.c:360
#42 0x000057d919724485 in _start ()
We can see this is happening while performing during a call to free at #10 , while freeing the chunk 0x57d94aa2e590 which corresponds to the memory block minus 16 bytes, i.e. 0x57d94aa2e580.
pwndbg> vis-heap-chunks 0x57d94aa2e580
0x57d94aa2e580 0x0000000000000000 0x0000000000000091 ................
0x57d94aa2e590 0x0000000000000004 0x00000000ff080000 ................
0x57d94aa2e5a0 0x0000000000000000 0x000057d94aa2e4a0 ...........J.W..
0x57d94aa2e5b0 0x000057d94aa2e480 0x0000000000000000 ...J.W..........
0x57d94aa2e5c0 0x0000000000000000 0x0000000000000000 ................
0x57d94aa2e5d0 0x0000000000000000 0x0000000000000000 ................
0x57d94aa2e5e0 0x0000000000000000 0x0000000000000000 ................
0x57d94aa2e5f0 0x0000000000000000 0x0000000000000000 ................
0x57d94aa2e600 0x0000000000000000 0x0000000000000000 ................
0x57d94aa2e610 0x0000000000000000 0x0000000000000031 ........1.......
0x57d94aa2e620 0x0b00000200000001 0x0000000100000008 ................
0x57d94aa2e630 0x0530303031303080 0x0029303946ff007f .001000....F90).
0x57d94aa2e640 0x000057d94aa2e620 0x000057d94aa2e5f8 ..J.W.....J.W..
0x57d94aa2e650 0x000057d94aa2e5d8 0x00000000ffff0500 ...J.W..........
0x57d94aa2e660 0x000057d94aa2f970 0x000057d94aa2e530 p..J.W..0..J.W..
0x57d94aa2e670 0x000057d94aa2e5f8 0x0000000020000000 ...J.W..... ....
In the glibc malloc implementation, the state of the previous chunk (free or in use) is determined by the PREV_INUSE bit stored in the size field of the current chunk. When this bit indicates that the previous chunk is free, the prev_size field of the current chunk contains the size of that previous chunk, allowing it to be located in memory. Based on this mechanism, the chunk at 0x57d94aa2e610 is considered free because the PREV_INUSE bit in the size field of the next chunk 0x57d94aa2e640 is cleared
Using pwndbg and the command on the chunk, we see:
pwndbg> heap -v 0x57d94aa2e580
Allocated chunk | PREV_INUSE
Addr: 0x57d94aa2e580
prev_size: 0x00
size: 0x90 (with flag bits: 0x91)
fd: 0x04
bk: 0xff080000
fd_nextsize: 0x00
bk_nextsize: 0x57d94aa2e4a0
Allocated chunk | PREV_INUSE
Addr: 0x57d94aa2e610
prev_size: 0x00
size: 0x30 (with flag bits: 0x31)
fd: 0xb00000200000001
bk: 0x100000008
fd_nextsize: 0x530303031303080
bk_nextsize: 0x29303946ff007f
Allocated chunk
Addr: 0x57d94aa2e640
prev_size: 0x57d94aa2e620
size: 0x57d94aa2e5f8 (with flag bits: 0x57d94aa2e5f8)
fd: 0x57d94aa2e5d8
bk: 0xffff0500
fd_nextsize: 0x57d94aa2f970
bk_nextsize: 0x57d94aa2e530
pwndbg> p/t 0x57d94aa2e620
$55 = 10101111101100101001010101000101110011000100000
During the call to free, execution enters the internal routine _int_free_merge_chunk. Due to corruption in the heap metadata, this function concludes that the previous chunk is free and therefore attempts a backward consolidation. _int_free_merge_chunk then calls _int_free_create_chunk, which is responsible for building the consolidated chunk and call to unlink_chunk causing at the end the error. By placing a hardware breakpoint in GDB on address 0x57d94aa2e630, we can identify when the memory overwrite happens in the function named vtkDICOMItem::FindDataElementOrInsert we will describe later
Reproducing this condition requires an understanding of how linked list structures are allocated in memory and how their internal linkage is maintained during runtime.
When a new sequence item is found, extracted from the malformed file, vtkDICOMItem::Set calls FindDataElementOrInsert LINE3 to get a pointer to the data-element node for that tag, then assigns tptr->Value = value LINE4. If the value is invalid LINE5-LINE13, it treats that as a delete request and unlinks the node from the list and decrements the element count NumberOfDataElements LINE12
LINE1 void vtkDICOMItem::Set(vtkDICOMTag tag, const vtkDICOMValue& v)
LINE2 {
LINE3 vtkDICOMDataElement *tptr = this->FindDataElementOrInsert(tag);
LINE4 tptr->Value = v;
LINE5 if (!v.IsValid())
LINE6 {
LINE7 // setting a value to the invalid value causes deletion
LINE8 tptr->Prev->Next = tptr->Next;
LINE9 tptr->Next->Prev = tptr->Prev;
LINE10 tptr->Next = nullptr;
LINE11 tptr->Prev = nullptr;
LINE12 this->L->NumberOfDataElements--;
LINE13 }
LINE14 }
vtkDICOMItem::FindDataElementOrInsert ensures if there’s no internal list container it creates one up to LINE19
Then it finds where the tag belongs the list is kept sorted by tag LINE35-LINE40 and, if the tag isn’t present LINE42, it inserts a new node by calling NewDataElement() LINE45, sets its tag, and links it into the doubly-linked list.LINE46-LINE52, and increment the element count represented by NumberOfDataElements LINE53
LINE16 vtkDICOMDataElement *vtkDICOMItem::FindDataElementOrInsert(vtkDICOMTag tag)
LINE17 {
LINE18 // make a container if we don't have one yet
LINE19 if (this->L == nullptr)
LINE20 {
LINE21 this->L = new List;
LINE22 this->L->Head.Next = &this->L->Tail;
LINE23 this->L->Tail.Prev = &this->L->Head;
LINE24 }
LINE25 // if we aren't the sole owner, copy before modifying
LINE26 else if (this->L->ReferenceCount != 1)
LINE27 {
LINE28 List *t = new List;
LINE29 vtkDICOMItem::CopyList(this->L, t);
LINE30 this->Clear();
LINE31 this->L = t;
LINE32 }
LINE33
LINE34 // find the insert location in the linked list
LINE35 vtkDICOMDataElement *tptr = &this->L->Tail;
LINE36 do
LINE37 {
LINE38 tptr = tptr->Prev;
LINE39 }
LINE40 while (tag < tptr->GetTag());
LINE41
LINE42 if (tag != tptr->GetTag())
LINE43 {
LINE44 // create a new data element
LINE45 vtkDICOMDataElement *e = this->NewDataElement(&tptr);
LINE46 e->Tag = tag;
LINE47 e->Prev = tptr;
LINE48 e->Next = tptr->Next;
LINE49 e->Prev->Next = e;
LINE50 e->Next->Prev = e;
LINE51
LINE52 tptr = e;
LINE53 this->L->NumberOfDataElements++;
LINE54 }
LINE55
LINE56 return tptr;
LINE57 }
vtkDICOMItem::NewDataElement called by vtkDICOMItem::FindDataElementOrInsert LINE45 is the allocator for the node storage.
This function allocates and returns a slot for a new vtkDICOMDataElement within vtkDICOMItem. It performs an initial allocation of four elements LINE67 if no array exists, and subsequently grows the array by doubling its size when the number of elements reaches a power-of-two threshold LINE70-LINE94unless a previously freed slot can be reused LINE73. During reallocation LINE75-LINE93, it rebuilds the internal linked list, updates any provided iterator to account for the new memory addresses, frees the old array, and finally returns the address of the available slot LINE96
LINE60 vtkDICOMDataElement *vtkDICOMItem::NewDataElement(vtkDICOMDataElement **iter)
LINE61 {
LINE62 int n = this->L->NumberOfDataElements;
LINE63
LINE64 // if no data elements yet, then allocate four
LINE65 if (this->L->DataElements == nullptr)
LINE66 {
LINE67 this->L->DataElements = new vtkDICOMDataElement[4];
LINE68 }
LINE69 // if n is a power of two, double allocated space
LINE70 else if (n >= 4 && (n & (n-1)) == 0)
LINE71 {
LINE72 // but first check if a free element exists
LINE73 do { --n; } while (n > 0 && this->L->DataElements[n].Next != nullptr);
LINE74
LINE75 if (this->L->DataElements[n].Next != nullptr)
LINE76 {
LINE77 // make a new, larger list
LINE78 n = this->L->NumberOfDataElements;
LINE79 vtkDICOMDataElement *oldptr = this->L->DataElements;
LINE80 this->L->DataElements = new vtkDICOMDataElement[2*n];
LINE81 vtkDICOMItem::CopyDataElements(
LINE82 this->L->Head.Next, &this->L->Tail, this->L);
LINE83 if (iter)
LINE84 {
LINE85 // fix the address of the provided node, since it was re-alloced
LINE86 vtkDICOMDataElement *tptr = *iter;
LINE87 vtkDICOMDataElement *nptr = &this->L->Tail;
LINE88 do { tptr = tptr->Next; nptr = nptr->Prev; }
LINE89 while (tptr != &this->L->Tail);
LINE90 *iter = nptr;
LINE91 }
LINE92 delete [] oldptr;
LINE93 }
LINE94 }
LINE95
LINE96 return &this->L->DataElements[n];
LINE97 }
A logic error in list management leads to a memory corruption vulnerability. When an element is deleted LINE5-13 during reverse traversal-specifically when the entry count is at a boundary-the NumberOfDataElements counter is decremented and pointers are NULLed. A subsequent call to vtkDICOMItem::FindDataElementOrInsert with a higher tag value triggers a break at LINE40. Consequently, vtkDICOMItem::NewDataElement returns the last entry based on the n correspondig to NumberOfDataElements , causing an overwrite of the current element due to an incorrect index calculation. When the deleted entry is not the last one, the function overwrites the current element instead of utilizing the freed slot. As the counter increments again, it reaches the boundary while the nullptr remains. During the next call, this prevents the memory buffer from doubling during reallocation, ultimately resulting in an out-of-bounds (OOB) write and heap metadata corruption, possibly leading to code execution.
corrupted size vs. prev_size
Program received signal SIGABRT, Aborted.
__pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
warning: 44 ./nptl/pthread_kill.c: No such file or directory
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────────────────────────────
RAX 0
*RBX 0xcd8
RCX 0xffffffffffffffff
*RDX 6
*RDI 0xcd8
*RSI 0xcd8
R8 0
*R9 0x72a1468b2440 (tunable_list) ◂— 'glibc.malloc.mxfast'
*R10 8
R11 0x246
*R12 6
*R13 0x7ffe494b1680 ◂— 4
*R14 0x16
*R15 0x7ffe494b1680 ◂— 4
*RBP 0x7ffe494b1540 —▸ 0x7ffe494b1560 —▸ 0x7ffe494b1620 —▸ 0x7ffe494b1740 —▸ 0x7ffe494b1750 ◂— ...
*RSP 0x7ffe494b1500 —▸ 0x72a1465deee0 (vtkDICOMReader::~vtkDICOMReader()) ◂— endbr64
*RIP 0x72a143a87b2c (pthread_kill+284) ◂— mov r14d, eax
─────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────────────────────────────────
► 0x72a143a87b2c <pthread_kill+284> mov r14d, eax R14D => 0
0x72a143a87b2f <pthread_kill+287> neg r14d
0x72a143a87b32 <pthread_kill+290> cmp eax, 0xfffff000 0x0 - 0xfffff000 EFLAGS => 0x207 [ CF PF af zf sf IF df of ac ]
0x72a143a87b37 <pthread_kill+295> mov eax, 0 EAX => 0
0x72a143a87b3c <pthread_kill+300> ✔ cmovbe r14d, eax
0x72a143a87b40 <pthread_kill+304> jmp pthread_kill+176 <pthread_kill+176>
↓
0x72a143a87ac0 <pthread_kill+176> mov rax, qword ptr [rbp - 0x38] RAX, [0x7ffe494b1508] => 0xc4dcb9962cc77600
0x72a143a87ac4 <pthread_kill+180> sub rax, qword ptr fs:[0x28] RAX => 0 (0xc4dcb9962cc77600 - 0xc4dcb9962cc77600)
0x72a143a87acd <pthread_kill+189> ✘ jne pthread_kill+341 <pthread_kill+341>
0x72a143a87ad3 <pthread_kill+195> add rsp, 0x18 RSP => 0x7ffe494b1518 (0x7ffe494b1500 + 0x18)
0x72a143a87ad7 <pthread_kill+199> mov eax, r14d EAX => 0
──────────────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffe494b1500 —▸ 0x72a1465deee0 (vtkDICOMReader::~vtkDICOMReader()) ◂— endbr64
01:0008│-038 0x7ffe494b1508 ◂— 0xc4dcb9962cc77600
02:0010│-030 0x7ffe494b1510 ◂— 2
03:0018│-028 0x7ffe494b1518 ◂— 6
04:0020│-020 0x7ffe494b1520 —▸ 0x72a14236fc00 ◂— 0x72a14236fc00
05:0028│-018 0x7ffe494b1528 —▸ 0x7ffe494b1680 ◂— 4
... ↓ 2 skipped
────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────────────
► 0 0x72a143a87b2c pthread_kill+284
1 0x72a143a87b2c pthread_kill+284
2 0x72a143a87b2c pthread_kill+284
3 0x72a143a2e27e raise+30
4 0x72a143a118ff abort+223
5 0x72a143a127b6 _IO_peekc_locked.cold
6 0x72a143a91ff5 None
7 0x72a143a92b96 unlink_chunk.isra+198
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2026-02-10 - Initial Vendor Contact
2026-03-03 - Vendor Disclosure
2026-06-01 - Vendor Patch Release
2026-06-25 - Public Release
Emmanuel Tacheau of Cisco Talos