Talos Vulnerability Report

TALOS-2026-2421

VTK vtkDEMReader ReadProfiles memory corruption vulnerability

October 29, 2026

CVE Number

CVE-2026-8822

Summary

A memory corruption vulnerability exists in the vtkDEMReader ReadProfiles functionality of vtk (version(s): 9.6.1). A specially crafted USGS DEM can lead to a buffer overflow. An attacker can provide a malicious file to trigger this vulnerability.

Confirmed Vulnerable Versions

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)

Product URLs

vtk - https://vtk.org/

CVSSv3 Score

7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CWE

CWE-787 - Out-of-bounds Write

Details

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.

vtkDEMReader parses the USGS Digital Elevation Model format — a fixed-width ASCII format. A Type A record (1024 bytes) describes grid geometry (ground coordinates, spatial resolution, row and column counts). B-records follow, each containing a profile: a 24-byte prefix of four 6-digit integers (profileId[0], profileId[1], profileSize[0], profileSize[1]) and then elevation samples.

The reader parses a malformed DEM sample without error. ASan reports:

[PoC] Update(./poc/seed_mut_oob_row_overflow.dem)
==364==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f23592b6a14
WRITE of size 4 at 0x7f23592b6a14 thread T0
    #0 in vtkDEMReader::ReadProfiles(vtkImageData*) at vtkDEMReader.cxx:440
    #1 in vtkDEMReader::RequestData(...) at vtkDEMReader.cxx:146
    [...]
    #10 in main at poc_vtkdemreader_update.cpp:20
0x7f23592b6a14 is located 0 bytes after 610836-byte region

The crash is a 4-byte heap write at LINE_440 inside ReadProfiles(), called from RequestData() at LINE_146. The write lands at address 0x7f23592b6a14, exactly 0 bytes after the end of the allocated region — a right-edge overrun.

The crash-site code in ReadProfiles():

LINE_325  int vtkDEMReader::ReadProfiles(vtkImageData* data)
LINE_326  {
  [...]
LINE_395  std::tie(profileId[0], profileId[1], profileSize[0], profileSize[1]) = resultInt4->values();
  [...]
LINE_416  rowId = profileId[0] - 1;
LINE_417  columnId = profileId[1] - 1;
LINE_418  lastRow = rowId + profileSize[0] - 1;
  [...]
LINE_429  for (row = rowId; row <= lastRow; row++)
LINE_430  {
LINE_431    auto resultInt = vtk::scan_value<int>(fp);
  [...]
LINE_439    elevation = resultInt->value();
LINE_440    *(outPtr + columnId + row * numberOfColumns) = elevation * units;
LINE_441  }
  [...]
}

At LINE_395, four integers are read from the B-record prefix. profileId[0] and profileSize[0] are attacker-controlled values parsed from the file. At LINE_416, rowId is derived from profileId[0]. At LINE_418, lastRow is computed from rowId + profileSize[0] - 1. The loop at LINE_429 iterates row from rowId to lastRow inclusive, and each iteration writes a float elevation value into the output buffer at LINE_440 using row * numberOfColumns as part of the offset.

outPtr points to the scalar buffer of the output vtkImageData. The buffer is allocated by RequestData() (frame #1 in the crash stack). At LINE_124, RequestData() allocates scalars based on the grid extent published during the information phase:

LINE_116  int vtkDEMReader::RequestData(vtkInformation* vtkNotUsed(request),
LINE_117    vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
LINE_118  {
  [...]
LINE_123    output->SetExtent(outInfo->Get(vtkStreamingDemandDrivenPipeline::WHOLE_EXTENT()));
LINE_124    output->AllocateScalars(outInfo);
  [...]
LINE_141    if (this->ReadTypeARecord() == 0)
LINE_142    {
LINE_146      this->ReadProfiles(output);
LINE_147    }
  [...]
LINE_152    return 1;
  [...]
}

AllocateScalars() at LINE_124 sizes the buffer from the extent — which encodes NumberOfRows × NumberOfColumns. At LINE_146, RequestData() calls ReadProfiles(output), passing the allocated vtkImageData. The buffer size is fixed at this point.

The extent itself is computed during the information phase. RequestInformation() calls ReadTypeARecord() at LINE_96 to parse the Type A header, then derives extent, origin, and spacing at LINE_99:

LINE_80   int vtkDEMReader::RequestInformation(vtkInformation* vtkNotUsed(request),
LINE_81     vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
LINE_82   {
  [...]
LINE_96     this->ReadTypeARecord();
LINE_99     this->ComputeExtentOriginAndSpacing(extent, origin, spacing);
  [...]
LINE_111    return 1;
  [...]
}

RequestInformation() returns 1 unconditionally at LINE_111. The return value of ReadTypeARecord() at LINE_96 is not checked. The pipeline therefore always proceeds to the data phase, regardless of whether the header was parsed successfully or produced consistent values.

The buffer is sized from the Type A record (grid dimensions), but the write loop in ReadProfiles() is driven by B-record fields (profileId[0], profileSize[0]). These two sets of values are parsed independently from different parts of the file, and no validation enforces consistency between them. Specifically, lastRow computed at LINE_418 must satisfy lastRow < NumberOfRows for the write at LINE_440 to remain within bounds. No such check exists between LINE_395 (where the B-record fields are parsed) and LINE_429 (where the loop begins).

Because profileId[0] and profileSize[0] are read directly from the file, an attacker controls: - the write offset — rowId and lastRow determine where in the buffer the writes land - the write count — profileSize[0] determines how many iterations the loop executes - the written content — elevation at LINE_439 is an attacker-controlled integer

The buffer size is also indirectly attacker-controlled through the Type A record’s ground coordinates and spatial resolution. A crafted DEM file that declares a small grid in the Type A record but provides a B-record profile spanning more rows than the grid contains causes ReadProfiles() to write past the end of the allocated buffer.

On non-instrumented builds, the heap corruption is silent — no error is raised and the process continues with a compromised heap state.

Crash Information

ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f23592b6a14 at pc 0x7f236b315aa0 bp 0x7ffedb81c2b0 sp 0x7ffedb81c2a0
WRITE of size 4 at 0x7f23592b6a14 thread T0
    #0 0x7f236b315a9f in vtkDEMReader::ReadProfiles(vtkImageData*) /opt/vtk-src/IO/Image/vtkDEMReader.cxx:440
    #1 0x7f236b311cc9 in vtkDEMReader::RequestData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/IO/Image/vtkDEMReader.cxx:146
    #2 0x7f236a190ed7 in vtkExecutive::CallAlgorithm(vtkInformation*, int, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkExecutive.cxx:723
    #3 0x7f236a16300b in vtkDemandDrivenPipeline::ExecuteData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:447
    #4 0x7f236a13efd8 in vtkCompositeDataPipeline::ExecuteData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkCompositeDataPipeline.cxx:151
    #5 0x7f236a15ffa9 in vtkDemandDrivenPipeline::ProcessRequest(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:246
    #6 0x7f236a2d159e in vtkStreamingDemandDrivenPipeline::ProcessRequest(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkStreamingDemandDrivenPipeline.cxx:331
    #7 0x7f236a1620b9 in vtkDemandDrivenPipeline::UpdateData(int) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:404
    #8 0x7f236a2d40ce in vtkStreamingDemandDrivenPipeline::Update(int, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkStreamingDemandDrivenPipeline.cxx:421
    #9 0x7f236a11d387 in vtkAlgorithm::Update(int) /opt/vtk-src/Common/ExecutionModel/vtkAlgorithm.cxx:1660
    #10 0x55be193039f7 in main /manuwork/advisories/dem-image-1/poc_vtkdemreader_update.cpp:20
    #11 0x7f235b4ca1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #12 0x7f235b4ca28a in __libc_start_main_impl ../csu/libc-start.c:360
    #13 0x55be19303404 in _start (/manuwork/advisories/dem-image-1/build-asan/poc_vtkdemreader_update+0x6404) (BuildId: a0662c635ccf9dae226cb5a66299c40ac4c1caaf)

0x7f23592b6a14 is located 0 bytes after 610836-byte region [0x7f2359221800,0x7f23592b6a14)
allocated by thread T0 here:
    #0 0x7f236bb5b9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x7f235e597ce3 in vtkBuffer<float>::Allocate(long long) /opt/vtk-src/Common/Core/vtkBuffer.h:202
    #2 0x7f235e6c3688 in vtkAOSDataArrayTemplate<float>::ReallocateTuples(long long) /opt/vtk-src/Common/Core/vtkAOSDataArrayTemplate.txx:488
    #3 0x7f235e6c3688 in vtkGenericDataArray<vtkAOSDataArrayTemplate<float>, float, 5>::ReallocateTuples(long long) /opt/vtk-src/Common/Core/vtkGenericDataArray.h:358
    #4 0x7f235e6c3688 in vtkGenericDataArray<vtkAOSDataArrayTemplate<float>, float, 5>::ReserveTuples(long long) /opt/vtk-src/Common/Core/vtkGenericDataArray.txx:455
    #5 0x7f23656243aa in vtkAbstractArray::ReserveValues(long long) /opt/vtk-src/Common/Core/vtkAbstractArray.cxx:216
    #6 0x7f23656243aa in vtkAbstractArray::SetNumberOfValues(long long) /opt/vtk-src/Common/Core/vtkAbstractArray.cxx:222
    #7 0x7f2368c465aa in vtkImageData::AllocateScalars(int, int) /opt/vtk-src/Common/DataModel/vtkImageData.cxx:1076
    #8 0x7f2368c45c5a in vtkImageData::AllocateScalars(vtkInformation*) /opt/vtk-src/Common/DataModel/vtkImageData.cxx:1034
    #9 0x7f236b311a31 in vtkDEMReader::RequestData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/IO/Image/vtkDEMReader.cxx:124
    #10 0x7f236a190ed7 in vtkExecutive::CallAlgorithm(vtkInformation*, int, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkExecutive.cxx:723
    #11 0x7f236a16300b in vtkDemandDrivenPipeline::ExecuteData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:447
    #12 0x7f236a13efd8 in vtkCompositeDataPipeline::ExecuteData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkCompositeDataPipeline.cxx:151
    #13 0x7f236a15ffa9 in vtkDemandDrivenPipeline::ProcessRequest(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:246
    #14 0x7f236a2d159e in vtkStreamingDemandDrivenPipeline::ProcessRequest(vtkInformation*, vtkInformationVector**, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkStreamingDemandDrivenPipeline.cxx:331
    #15 0x7f236a1620b9 in vtkDemandDrivenPipeline::UpdateData(int) /opt/vtk-src/Common/ExecutionModel/vtkDemandDrivenPipeline.cxx:404
    #16 0x7f236a2d40ce in vtkStreamingDemandDrivenPipeline::Update(int, vtkInformationVector*) /opt/vtk-src/Common/ExecutionModel/vtkStreamingDemandDrivenPipeline.cxx:421
    #17 0x7f236a11d387 in vtkAlgorithm::Update(int) /opt/vtk-src/Common/ExecutionModel/vtkAlgorithm.cxx:1660
    #18 0x55be193039f7 in main /manuwork/advisories/dem-image-1/poc_vtkdemreader_update.cpp:20
    #19 0x7f235b4ca1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #20 0x7f235b4ca28a in __libc_start_main_impl ../csu/libc-start.c:360
    #21 0x55be19303404 in _start (/manuwork/advisories/dem-image-1/build-asan/poc_vtkdemreader_update+0x6404) (BuildId: a0662c635ccf9dae226cb5a66299c40ac4c1caaf)

SUMMARY: AddressSanitizer: heap-buffer-overflow /opt/vtk-src/IO/Image/vtkDEMReader.cxx:440 in vtkDEMReader::ReadProfiles(vtkImageData*)
Shadow bytes around the buggy address:
  0x7f23592b6780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7f23592b6800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7f23592b6880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7f23592b6900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7f23592b6980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7f23592b6a00: 00 00[04]fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7f23592b6a80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7f23592b6b00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7f23592b6b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7f23592b6c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7f23592b6c80: 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
==364==ABORTING

Vendor Response (CVE-2026-8822)

Vendor Link: https://vtk.org/download/

Timeline

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

Credit

Emmanuel Tacheau of Cisco Talos