TALOS-2026-2422
CVE-2026-8824
A memory corruption vulnerability exists in the vtkBMPReader ExecuteDataWithInformation functionality of vtk (version(s): 9.6.1). A specially crafted .bmp file can lead to a buffer overflow. 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 (version(s): 9.6.1)
vtk - https://vtk.org/
7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-787 - Out-of-bounds Write
VTK, or the Visualization Toolkit, is an open-source software system for 3D computer graphics, image processing, and scientific visualization. It provides a large collection of algorithms and data structures for rendering, volume visualization, geometric processing, and interaction.
vtkBMPReader parses the Windows Bitmap (BMP) image format.
The reader parses a malformed BMP sample. ASan reports:
vtkBMPReader.cxx:147 ERR| vtkBMPReader: Only BMP depths of (8,24) are supported. Not 16
==16==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000007ff2
WRITE of size 2 at 0x502000007ff2 thread T0
#0 in void vtkBMPReaderUpdate2<short>(vtkBMPReader*, vtkImageData*, short*)
/opt/vtk-src/IO/Image/vtkBMPReader.cxx:428:22
#1 in vtkBMPReader::ExecuteDataWithInformation(...)
/opt/vtk-src/IO/Image/vtkBMPReader.cxx:476:5
0x502000007ff2 is located 0 bytes after 2-byte region [0x502000007ff0,0x502000007ff2)
A heap-buffer-overflow WRITE of 2 bytes at LINE_428 inside vtkBMPReaderUpdate2<short>(), called from ExecuteDataWithInformation() at LINE_476. The write lands at address 0x502000007ff2, exactly 0 bytes after the end of a 2-byte allocated region. The error message on the first line — emitted before the crash — indicates that the reader recognized the bit depth as unsupported.
The crash-site code in vtkBMPReaderUpdate2():
LINE_284 template <class OT>
LINE_285 void vtkBMPReaderUpdate2(vtkBMPReader* self, vtkImageData* data, OT* outPtr)
LINE_286 {
[...]
LINE_415 if (self->GetDepth() == 8 && !Keep8bit)
LINE_416 {
LINE_417 outPtr0[0] = (OT)(Colors[inPtr[0] * 3]);
LINE_418 outPtr0[1] = (OT)(Colors[inPtr[0] * 3 + 1]);
LINE_419 outPtr0[2] = (OT)(Colors[inPtr[0] * 3 + 2]);
LINE_420 }
LINE_421 else if (self->GetDepth() == 8 && Keep8bit)
LINE_422 {
LINE_423 outPtr0[0] = (OT)(inPtr[0]);
LINE_424 }
LINE_425 else
LINE_426 {
LINE_427 outPtr0[0] = (OT)(inPtr[2]);
LINE_428 outPtr0[1] = (OT)(inPtr[1]);
LINE_429 outPtr0[2] = (OT)(inPtr[0]);
LINE_430 }
[...]
}
self->GetDepth() returns the bit depth read from the BMP header — a member variable this->Depth. The first two branches handle depth 8. The else branch at LINE_425 is a catch-all: any depth that is not 8 enters this branch, which writes 3 components per pixel into the output buffer (outPtr0[0], outPtr0[1], outPtr0[2] at LINE_427–LINE_429). outPtr0 is a pointer into outPtr, which was passed from the caller.
One level up in the call stack (frame #1), ExecuteDataWithInformation() allocates the output buffer and dispatches to vtkBMPReaderUpdate2():
LINE_451 void vtkBMPReader::ExecuteDataWithInformation(vtkDataObject* output, vtkInformation* outInfo)
LINE_452 {
LINE_453 vtkImageData* data = this->AllocateOutputData(output, outInfo);
[...]
LINE_467 this->ComputeDataIncrements();
[...]
LINE_473 outPtr = data->GetScalarPointer();
LINE_474 switch (data->GetScalarType())
LINE_475 {
LINE_476 vtkTemplateMacro(vtkBMPReaderUpdate2(this, data, static_cast<VTK_TT*>(outPtr)));
[...]
}
AllocateOutputData() at LINE_453 sizes the output buffer based on the scalar component count — a value configured during the information phase. GetScalarPointer() at LINE_473 returns a pointer to that buffer. At LINE_476, the function dispatches into vtkBMPReaderUpdate2() with this (which carries this->Depth) and outPtr. No validation of this->Depth occurs between LINE_451 and LINE_476.
The buffer is sized by the scalar component count. That count is set in ExecuteInformation(), which also reads the bit depth from the file header:
LINE_42 void vtkBMPReader::ExecuteInformation()
LINE_43 {
[...]
LINE_138 if (stream->Read(&this->Depth, 2) != 2)
LINE_139 {
LINE_140 vtkErrorMacro("Error reading depth");
LINE_141 return;
LINE_142 }
LINE_143 vtkByteSwap::Swap2LE(&this->Depth);
LINE_145 if ((this->Depth != 8) && (this->Depth != 24))
LINE_146 {
LINE_147 vtkErrorMacro("Only BMP depths of (8,24) are supported. Not " << this->Depth);
LINE_148 return;
LINE_149 }
[...]
LINE_218 this->SetDataScalarTypeToUnsignedChar();
LINE_219 if ((this->Depth == 8) && this->Allow8BitBMP)
LINE_220 {
LINE_221 this->SetNumberOfScalarComponents(1);
LINE_222 }
LINE_223 else
LINE_224 {
LINE_225 this->SetNumberOfScalarComponents(3);
LINE_226 }
[...]
}
this->Depth is a 2-byte integer read from the BMP DIB header at LINE_138 — its value is attacker-controlled. At LINE_145, the function checks whether the depth is 8 or 24. For any other value, it emits an error message at LINE_147 and returns at LINE_148.
The code at LINE_218–LINE_226 configures the scalar component count: 1 for 8-bit grayscale, 3 for 24-bit RGB. The early return at LINE_148 exits the function before this code is reached. The scalar component count is therefore left at whatever default the reader object holds — typically 1.
The pipeline allocates the output buffer based on that component count. With 1 component, the buffer has room for one value per pixel. But the else branch in vtkBMPReaderUpdate2() writes 3 values per pixel — the buffer is too small by a factor of 3.
VTK’s pipeline dispatches REQUEST_INFORMATION and REQUEST_DATA as separate requests. ExecuteInformation() is called during the information phase; ExecuteDataWithInformation() during the data phase. The early return at LINE_148 exits the information phase — but does not prevent the data phase from running. The parent class vtkImageReader2::RequestInformation() calls this->ExecuteInformation() and then checks this->GetErrorCode() to decide whether to report failure. vtkBMPReader::ExecuteInformation() never calls this->SetErrorCode() before returning at LINE_148 — GetErrorCode() returns NoError, RequestInformation() returns success, and the pipeline proceeds to the data phase.
The data phase runs with this->Depth still holding the attacker-controlled value (unchanged since LINE_138), while the buffer was sized for a component count that was never configured for that depth. The else branch at LINE_425 writes 3 components into a 1-component buffer, producing the heap overflow.
The attacker controls the content written: the values at LINE_427–LINE_429 come from inPtr, which points to raw pixel data from the BMP file. The attacker also controls which branch is taken: any bit depth value other than 8 enters the else branch at LINE_425, which writes 3 components per pixel into a buffer sized for 1.
The observed crash is an out-of-bounds write into heap memory. Because the write lands immediately past the allocated buffer boundary, it corrupts adjacent heap metadata or application objects. On non-instrumented builds this corruption is silent — no error is raised and the process continues with a compromised heap state.
2026-05-04 13:57:49.309 ( 0.127s) [ 7F455270CDC0] vtkBMPReader.cxx:147 ERR| vtkBMPReader (0x516000001e80): Only BMP depths of (8,24) are supported. Not 16
=================================================================
==16==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000007ff2 at pc 0x7f456fc27fe8 bp 0x7ffd4c90f8d0 sp 0x7ffd4c90f8c8
WRITE of size 2 at 0x502000007ff2 thread T0
#0 0x7f456fc27fe7 in void vtkBMPReaderUpdate2<short>(vtkBMPReader*, vtkImageData*, short*) /opt/vtk-src/IO/Image/vtkBMPReader.cxx:428:22
#1 0x7f456fc00e46 in vtkBMPReader::ExecuteDataWithInformation(vtkDataObject*, vtkInformation*) /opt/vtk-src/IO/Image/vtkBMPReader.cxx:476:5
#2 0x7f456eaa765f in vtkImageAlgorithm::RequestData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkImageAlgorithm.cxx:61:11
#3 0x7f456ea5fed7 in vtkExecutive::CallAlgorithm(vtkInformation*, int, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkExecutive.cxx:723:33
#4 0x7f456ea3200b in vtkDemandDrivenPipeline::ExecuteData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:447:22
#5 0x7f456ea0dfd8 in vtkCompositeDataPipeline::ExecuteData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkCompositeDataPipeline.cxx:151:32
#6 0x7f456ea2efa9 in vtkDemandDrivenPipeline::ProcessRequest(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:246:22
#7 0x7f456eba059e in vtkStreamingDemandDrivenPipeline::ProcessRequest(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkStreamingDemandDrivenPipeline.cxx:331:27
#8 0x7f456ea310b9 in vtkDemandDrivenPipeline::UpdateData(int) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:404:16
#9 0x7f456eba30ce in vtkStreamingDemandDrivenPipeline::Update(int, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkStreamingDemandDrivenPipeline.cxx:421:34
#10 0x7f456e9ec387 in vtkAlgorithm::Update(int) /opt/vtk-src/Common/ExecutionModel/vtkAlgorithm.cxx:1660:32
#11 0x55c0556074aa in main /manuwork/medical_harness_vtk/medical_harness_vtk.cpp:775:13
#12 0x7f456039c1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#13 0x7f456039c28a in __libc_start_main csu/../csu/libc-start.c:360:3
#14 0x55c05551b964 in _start (/manuwork/medical_harness_vtk/build-afl/medical_harness_vtk+0x2f964) (BuildId: 88f0ec43a4e0ed3bc1bf4f4c79d2279164013566)
0x502000007ff2 is located 0 bytes after 2-byte region [0x502000007ff0,0x502000007ff2)
allocated by thread T0 here:
#0 0x55c0555bb793 in malloc (/manuwork/medical_harness_vtk/build-afl/medical_harness_vtk+0xcf793) (BuildId: 88f0ec43a4e0ed3bc1bf4f4c79d2279164013566)
#1 0x7f456577e15f in vtkBuffer<short>::Allocate(long long) /opt/vtk-src/Common/Core/vtkBuffer.h:202:43
#2 0x7f45658a8958 in vtkAOSDataArrayTemplate<short>::ReallocateTuples(long long) /opt/vtk-src/Common/Core/vtkAOSDataArrayTemplate.txx:488:21
#3 0x7f45658a8958 in vtkGenericDataArray<vtkAOSDataArrayTemplate<short>, short, 5>::ReallocateTuples(long long) /opt/vtk-src/Common/Core/vtkGenericDataArray.h:358:42
#4 0x7f45658a8958 in vtkGenericDataArray<vtkAOSDataArrayTemplate<short>, short, 5>::ReserveTuples(long long) /opt/vtk-src/Common/Core/vtkGenericDataArray.txx:455:14
#5 0x7f4569ef33aa in vtkAbstractArray::ReserveValues(long long) /opt/vtk-src/Common/Core/vtkAbstractArray.cxx:216:16
#6 0x7f4569ef33aa in vtkAbstractArray::SetNumberOfValues(long long) /opt/vtk-src/Common/Core/vtkAbstractArray.cxx:222:14
#7 0x7f456d5155aa in vtkImageData::AllocateScalars(int, int) /opt/vtk-src/Common/DataModel/vtkImageData.cxx:1076:12
#8 0x7f456eaa8c2a in vtkImageAlgorithm::AllocateOutputData(vtkDataObject*, vtkInformation*) /opt/vtk-src/Common/ExecutionModel/vtkImageAlgorithm.cxx:201:11
#9 0x7f456fc00750 in vtkBMPReader::ExecuteDataWithInformation(vtkDataObject*, vtkInformation*) /opt/vtk-src/IO/Image/vtkBMPReader.cxx:453:30
#10 0x7f456eaa765f in vtkImageAlgorithm::RequestData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkImageAlgorithm.cxx:61:11
#11 0x7f456ea5fed7 in vtkExecutive::CallAlgorithm(vtkInformation*, int, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkExecutive.cxx:723:33
#12 0x7f456ea3200b in vtkDemandDrivenPipeline::ExecuteData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:447:22
#13 0x7f456ea0dfd8 in vtkCompositeDataPipeline::ExecuteData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkCompositeDataPipeline.cxx:151:32
#14 0x7f456ea2efa9 in vtkDemandDrivenPipeline::ProcessRequest(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:246:22
#15 0x7f456eba059e in vtkStreamingDemandDrivenPipeline::ProcessRequest(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkStreamingDemandDrivenPipeline.cxx:331:27
#16 0x7f456ea310b9 in vtkDemandDrivenPipeline::UpdateData(int) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:404:16
#17 0x7f456eba30ce in vtkStreamingDemandDrivenPipeline::Update(int, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkStreamingDemandDrivenPipeline.cxx:421:34
#18 0x7f456e9ec387 in vtkAlgorithm::Update(int) /opt/vtk-src/Common/ExecutionModel/vtkAlgorithm.cxx:1660:32
#19 0x55c0556074aa in main /manuwork/medical_harness_vtk/medical_harness_vtk.cpp:775:13
#20 0x7f456039c1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#21 0x7f456039c28a in __libc_start_main csu/../csu/libc-start.c:360:3
#22 0x55c05551b964 in _start (/manuwork/medical_harness_vtk/build-afl/medical_harness_vtk+0x2f964) (BuildId: 88f0ec43a4e0ed3bc1bf4f4c79d2279164013566)
SUMMARY: AddressSanitizer: heap-buffer-overflow /opt/vtk-src/IO/Image/vtkBMPReader.cxx:428:22 in void vtkBMPReaderUpdate2<short>(vtkBMPReader*, vtkImageData*, short*)
Shadow bytes around the buggy address:
0x502000007d00: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x502000007d80: fa fa 00 fa fa fa fd fa fa fa 07 fa fa fa fd fa
0x502000007e00: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x502000007e80: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x502000007f00: fa fa 00 fa fa fa 00 fa fa fa 00 fa fa fa 00 fa
=>0x502000007f80: fa fa 00 fa fa fa 00 fa fa fa fd fd fa fa[02]fa
0x502000008000: fa fa 00 fa fa fa 00 00 fa fa 00 00 fa fa 00 00
0x502000008080: fa fa 00 00 fa fa 00 01 fa fa 01 fa fa fa 04 fa
0x502000008100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000008180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000008200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==16==ABORTING
Vendor Link: https://vtk.org/download/
2026-06-23 - Vendor Notification
2026-06-25 - Vendor Acknowledged
2026-06-30 - 7-Day
2026-07-02 - Follow-Up Sent
2026-07-07 - Vendor Replied, Dev Team Working
2026-08-07 - 45-Day
2026-08-22 - 60-Day
2026-09-15 - Follow-Up Sent
2026-09-21 - 90-Day
2026-09-23 - Vendor Replied, Dev Team Working
2026-10-12 - Advised Vendor Cisco Will Publish And Close Out
Emmanuel Tacheau of Cisco Talos