Skip to content

Fix/stickify allocation overflow chain - #3618

Draft
jonathanhuang-bot wants to merge 2 commits into
onnx:mainfrom
jonathanhuang-bot:fix/stickify-allocation-overflow-chain
Draft

Fix/stickify allocation overflow chain#3618
jonathanhuang-bot wants to merge 2 commits into
onnx:mainfrom
jonathanhuang-bot:fix/stickify-allocation-overflow-chain

Conversation

@jonathanhuang-bot

Copy link
Copy Markdown
Collaborator

Jonathan Huang added 2 commits August 20, 2026 10:50
getsize_ztensor computed dim4*dim3*ceil(dim2/sticks)*ceil(dim1/cells)*4096
as a plain uint64_t chain of multiplications with no overflow check. Each
dimension is a uint32_t from an attacker-controlled zdnn_tensor_desc. With
the per-dim and total-size validation in verify_transformed_descriptor
disabled (#if 0), crafted dims can wrap the product to a tiny value.
malloc_aligned_4k then succeeds with a tiny buffer, and the subsequent
transform_ztensor call writes data sized to the full un-wrapped logical
dimensions past the end of that buffer.

Hunk 1: wrap each factor in __builtin_mul_overflow and return 0 on any
overflow. malloc_aligned_4k already has an existing !size early-out
(Stickify.cpp:272) that fails closed on this 0 return.

Hunk 2: in stickify()'s pre-flight buffer-size check, cache the
getsize_ztensor result and explicitly reject !required_size alongside the
existing size-comparison, so a 0 from an overflowing product is caught
before any buffer access rather than passing the size comparison by
coincidence.

Signed-off-by: Jonathan Huang <jonathanhuang@ibm.com>
…k wrap

This commit completes the three-part fix for the Stickify allocation
overflow chain (f006 + f018), building on the getsize_ztensor overflow
guard already committed for f013.

--- f006: Re-enable verify_transformed_descriptor dim/size checks ---

The entire validation body of verify_transformed_descriptor() was
wrapped in #if 0 since 2025-01-08 (commit 86dbaf0, issue onnx#3034)
because the real zDNN runtime calls zdnn_get_max_for_dim() /
zdnn_get_nnpa_max_tensor_size(), which read nnpa_query_result state
that is not initialized in onnx-mlir's compile-time stickify build.

The #if 0 block is removed and replaced with equivalent checks using
compile-time constants from NNPALimit.hpp (NNPAGetMaxForDim /
NNPA_MAXIMUM_TENSOR_SIZE), which are already used elsewhere in the
NNPA path and do not require nnpa_query_result to be initialized.

The zero-dim check and the total-size check were the only guards
anywhere in the pipeline for those two conditions. Without them,
attacker-controlled dims from a crafted .onnx Constant flow directly
into getsize_ztensor / allochelper_ztensor_alloc with no bound at all,
enabling the f013 overflow and the f018 wrap described below.

--- f018: Fix dead SIZE_MAX check in malloc_aligned_4k ---

The guard 'if (!size || size > SIZE_MAX)' has a dead second condition:
size_t can never exceed SIZE_MAX by definition. The actual hazard is
'malloc(size + extra_allocation)' on the next line: if size lands
within extra_allocation (4103 bytes) of SIZE_MAX, the addition wraps
size_t to a tiny value, malloc succeeds with a tiny block, but the
caller records the original near-SIZE_MAX value as buffer_size. Any
subsequent write trusting buffer_size (memset / transform_ztensor)
then writes far past the real allocation.

Fix: replace 'size > SIZE_MAX' with 'size > SIZE_MAX - extra_allocation'
so the check correctly catches the wrap before malloc is called.

Signed-off-by: Jonathan Huang <jonathanhuang@ibm.com>
@jenkins-droid

Copy link
Copy Markdown
Collaborator

Can one of the admins verify this patch?

@jonathanhuang-bot

Copy link
Copy Markdown
Collaborator Author

Summary

This PR fixes three related security defects in src/Accelerators/NNPA/Support/Stickify/Stickify.cpp that form a chain: the first disables the gate that should reject bad inputs, the second is an integer overflow that fires when bad inputs reach it, and the third is a dead safety check that was supposed to catch the overflow but never could. All three must be fixed together for the allocation to be safe.

When onnx-mlir compiles a model targeting the NNPA hardware accelerator, weight constants are converted to the NNPA hardware layout ("stickified") at compile time. This process allocates a buffer sized from the tensor's declared dimensions, then writes the converted data into it. The three bugs below all affect that allocation sequence in the same file.

f006 — The validation gate was entirely disabled

verify_transformed_descriptor() is supposed to check that a tensor's dimensions are non-zero and within NNPA's hardware limits before any memory is allocated. The entire body of this function was wrapped in #if 0 since January 2025 (commit 86dbaf0, issue #3034) because the real zDNN library's limit-query functions require hardware state that isn't available at compile time. The function unconditionally returned OK for every input.
Fix: The #if 0 block is removed. The two broken runtime calls are replaced with equivalent compile-time constants from NNPALimit.hpp (NNPAGetMaxForDim / NNPA_MAXIMUM_TENSOR_SIZE), which are already used elsewhere in the NNPA path and don't require hardware state. The zero-dim and total-size checks — which had no equivalent anywhere else in the pipeline — are now active again.

f013 — The buffer size calculation could overflow

getsize_ztensor() computes how many bytes to allocate as a chain of five multiplications: dim4 × dim3 × ceil(dim2/32) × ceil(dim1/cells) × 4096. Each dimension is an attacker-controlled 32-bit integer. With f006's gate disabled, there was nothing to bound the inputs, so a crafted tensor could cause this product to wrap around uint64_t to a tiny value — for example, choosing dims whose product would be 2^64 + 4096 causes the result to be just 4096. That tiny value drives malloc, giving a 4 KB buffer, while transform_ztensor then writes data sized for the full logical dimensions into it — a heap buffer overflow.
Fix: Each multiplication in the chain is replaced with __builtin_mul_overflow. If any step would overflow, the function returns 0 instead of a wrapped value. The existing !size check in malloc_aligned_4k catches that 0 and returns NULL, so the allocation fails cleanly with no memory corruption.

f018 — A safety check in the allocator was dead code

malloc_aligned_4k() needs a small amount of extra memory beyond the requested size (4103 bytes) for page-alignment bookkeeping, so it calls malloc(size + 4103). Before doing so it checks if (!size || size > SIZE_MAX). The second condition — size > SIZE_MAX — can never be true: size is of type size_t, and SIZE_MAX is by definition the largest value a size_t can hold. The check was therefore dead code. The real danger is the addition: if size is within 4103 of SIZE_MAX, then size + 4103 wraps around to a tiny number, malloc succeeds with a tiny block, but the caller records the original near-SIZE_MAX value as the buffer's size. Any subsequent write that trusts that recorded size walks far past the real allocation.
Fix: The dead check size > SIZE_MAX is replaced with size > SIZE_MAX - extra_allocation. This correctly catches any size that would cause the addition to wrap, and returns NULL before malloc is called.

@AlexandreEichenberger AlexandreEichenberger left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

if (__builtin_mul_overflow(
size, static_cast<uint64_t>(tfrmd_desc->dim3), &size) ||
__builtin_mul_overflow(
size, static_cast<uint64_t>(CEIL(number_of_sticks,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe the ceil could be computed before the test to improve readability

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at using these, but code assistant indicated that there is already support in LLVM that is more portable:

LLVM — two candidates in [MathExtras.h]
(llvm-project/llvm/include/llvm/Support/MathExtras.h):

llvm::SaturatingMultiply in line :638
llvm::MulOverflow in line 753

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants