Skip to content

Out-of-bounds read in the CPU embedding kernel when padding_idx is set (heap memory disclosure + remote crash) #79491

Description

@Pandya-mayur

bug描述 Describe the Bug

  • Affected component: paddle/phi/kernels/cpu/embedding_kernel.cc (EmbeddingCPUFunctor::apply)
  • Public API: paddle.nn.Embedding / paddle.nn.functional.embedding (with padding_idx set)
  • Severity: High
  • CVSS 3.1: 8.2 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H)
  • CWE: CWE-125 Out-of-bounds Read
  • Status: Confirmed on paddlepaddle==3.3.0 (heap disclosure + SIGSEGV) and via AddressSanitizer
  • Affected versions: v3.3.0 release tag and current develop HEAD (verified)

Affected Component

File: paddle/phi/kernels/cpu/embedding_kernel.cc   (line 53 on develop HEAD)
Function: phi::EmbeddingCPUFunctor<T, Context>::apply

Vulnerable Code

for (int64_t i = 0; i < ids_numel; ++i) {
  if (padding_idx_ == kNoPadding && ids[i] != padding_idx_) {   // <-- gate
    PADDLE_ENFORCE_LT(ids[i], row_number, ...);   // range check runs ONLY
    PADDLE_ENFORCE_GE(ids[i], 0, ...);            //   when no padding_idx
  }
}
...
for (int64_t i = 0; i < ids_numel; ++i) {
  if (padding_idx_ != kNoPadding && ids[i] == padding_idx_) {
    memset(output + i * row_width, 0, row_width * sizeof(T));
  } else {
    memcpy(output + i * row_width,
           table + ids[i] * row_width,            // <-- OOB: ids[i] unchecked
           row_width * sizeof(T));
  }
}

Root Cause

The id-range validation is incorrectly scoped: it is nested inside
if (padding_idx_ == kNoPadding && ...). kNoPadding is -1, so as soon as an embedding
layer is created with any padding_idx (e.g. the ubiquitous padding_idx=0 for NLP
models), the bounds check is skipped for every id, and the copy loop indexes the weight table
with an attacker-controlled ids[i] (int64). Only the padding-row zeroing should be
conditional on padding_idx; the range check must be unconditional.

Proof of Concept (public API, paddlepaddle==3.3.0)

(a) Heap memory disclosure — an out-of-range id returns raw heap bytes past the table:

import paddle
paddle.set_device("cpu")
emb = paddle.nn.Embedding(16, 8, padding_idx=0)     # valid ids: 0..15
out = emb(paddle.to_tensor([[40]], dtype="int64"))  # id 40 is out of range
print(out.numpy().reshape(-1))
# -> [1.11e+27, 1.36e-14, 2.81e+23, 1.46e-19, 0.0, 7.14e+31, 0.0, 0.0]
#    (uninitialised heap contents beyond the weight matrix)

(b) Remote crash (SIGSEGV) — a large id dereferences an unmapped page:

emb = paddle.nn.Embedding(16, 8, padding_idx=0)
emb(paddle.to_tensor([[10**9]], dtype="int64"))

Evidence

Real-wheel crash with Paddle's own C++ traceback naming the exact sink:

C++ Traceback (most recent call last):
0   paddle::pybind::eager_api_embedding(...)
2   paddle::experimental::embedding(...)
3   void phi::EmbeddingKernel<float, phi::CPUContext>(...)
4   void phi::EmbeddingCPUFunctor<float, phi::CPUContext>::apply<long>()
FatalError: `Segmentation fault` is detected by the operating system.
  [SignalInfo: *** SIGSEGV (@0x78f89e000) received by PID ... ]

AddressSanitizer proof (line-for-line excerpt of the kernel logic):

==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5150000002c0 ...
READ of size 32 at 0x5150000002c0 thread T0
    #0 memcpy
    #1 EmbeddingCPUFunctor_apply<float>  embedding_oob_harness.cpp:49
0x5150000002c0 is located 64 bytes after 512-byte region [0x515000000080,0x515000000280)
allocated by thread T0 here:
    #0 operator new(unsigned long)
    #6 std::vector<float>::vector(...)
    #7 main  embedding_oob_harness.cpp:62
SUMMARY: AddressSanitizer: heap-buffer-overflow in memcpy
Image Image Image Image

exploit_embedding_oob.py
validate.py

asan_crash.txt

How to Reproduce

  1. pip install paddlepaddle==3.3.0
  2. Run snippet (a) → an out-of-range id (40) returns a vector of raw heap values instead
    of raising — heap disclosure.
  3. Run snippet (b) → the process crashes with SIGSEGV in EmbeddingCPUFunctor::apply.
  4. (Optional, deterministic) Compile the kernel-logic excerpt with AddressSanitizer
    (g++ -fsanitize=address ...) or reproduce (b) on a cmake -DWITH_ASAN=ON build to
    obtain the in-binary trace.

Impact / Real-World Scenario

Embedding layers turn caller-supplied token ids into vectors. Any service that embeds
externally-influenced ids (text classification / retrieval / recommender inference that accept
tokenized input, or pipelines where a tokenizer/vocabulary mismatch can emit ids ≥
num_embeddings) is exposed:

  • Confidentiality (High): the output vector returns raw heap bytes adjacent to the weight
    table — potentially other tensors/buffers/secrets in the process heap.
  • Availability (High): a single crafted id crashes an inference worker.
  • The attacker-controlled read offset (ids[i] * row_width, ids[i] up to int64) is a
    flexible OOB-read primitive.

Suggested Remediation

Make the range check unconditional:

for (int64_t i = 0; i < ids_numel; ++i) {
  PADDLE_ENFORCE_GE(ids[i], 0, ...);
  PADDLE_ENFORCE_LT(ids[i], row_number, ...);
}

Keep only the padding-row zeroing conditional on padding_idx. Apply the same fix to the
GPU/XPU embedding kernels, which share the pattern.

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