Skip to content

x32-unpool: bound the index map before dereferencing the output pointers (OOB write)#10454

Merged
copybara-service[bot] merged 1 commit into
google:masterfrom
evilgensec:fix/unpool-index-bounds
Jul 2, 2026
Merged

x32-unpool: bound the index map before dereferencing the output pointers (OOB write)#10454
copybara-service[bot] merged 1 commit into
google:masterfrom
evilgensec:fix/unpool-index-bounds

Conversation

@evilgensec

Copy link
Copy Markdown
Contributor

Problem

The x32 unpooling microkernels (xnn_x32_unpool_ukernel__{scalar,neon,sse2,wasmsimd}) copy each channel to output[index[c]]:

const uint32_t i = *index++;
*((uint32_t*) ((uintptr_t) output[i] + offset)) = *input++;   // i unbounded

output is the per-pixel slice of the indirection buffer and holds exactly kernel_elements (= pooling_height * pooling_width) valid pointers. index is the caller-supplied index map passed to xnn_setup_unpooling2d_nhwc_x32. The index value selects an output pointer with no check that index[c] < kernel_elements, so an out-of-range index reads a pointer past the indirection slice and then writes the input element through that out-of-bounds pointer — a heap out-of-bounds read of the pointer table plus a write through an attacker-influenced wild pointer.

This is reachable from the public unpooling operator API with an untrusted index map (e.g. an unpooling op in a model whose index tensor is attacker-influenced rather than produced by a matching argmax pooling).

Reproduced under AddressSanitizer (pooling 2×2 so kernel_elements = 4, batch=in_h=in_w=channels=1 so the indirection slice is 4 pointers / 32 bytes, index[0] = 4):

==ERROR: AddressSanitizer: heap-buffer-overflow  READ of size 8
  #0 xnn_x32_unpool_ukernel__neon          x32-unpool-neon.c:44
  #1 pthreadpool_parallelize_2d
  #2 xnn_run_operator_with_index
0x... is located 0 bytes after 32-byte region   (the indirection slice from xnn_reshape_unpooling2d_nhwc_x32)

For a larger index the wild-pointer write also faults outright. Sibling asymmetry: the max-pooling indirection initializer clamps every computed index to input-1; unpooling trusts the caller's index verbatim.

Fix

Save the number of output pointers (kernel_elements) and bound each index against it before the indirect write, in all four variants. Indices outside the range are dropped (the output is pre-initialized with the fill value, so a dropped index leaves that location at fill). Valid unpooling indices (produced by argmax pooling) are always < kernel_elements, so well-formed input is unaffected.

Testing

  • The crafted index above is now ignored (no AddressSanitizer report).
  • A valid in-range index (e.g. index[0] = 2) still runs cleanly and writes to the correct output location, with unchanged output dimensions.

The x32 unpooling microkernels copy each channel to output[index[c]],
where index is the caller-supplied index map and output is the per-pixel
indirection slice holding exactly kernel_elements pointers. The index
value was used to select an output pointer with no check that it is less
than kernel_elements, so an out-of-range index reads a pointer past the
indirection buffer and then writes the input element through that
out-of-bounds pointer (heap out-of-bounds read of the pointer table plus
a write through an attacker-influenced wild pointer).

Guard each index against the number of output pointers and drop values
that fall outside it, in all four variants (scalar, neon, sse2,
wasmsimd). Valid unpooling indices (produced by argmax pooling) are
always < kernel_elements, so well-formed input is unaffected; the output
is pre-initialized with the fill value, so a dropped index simply leaves
that location at fill.
@copybara-service copybara-service Bot merged commit d7d90f9 into google:master Jul 2, 2026
7 checks passed
@evilgensec

Copy link
Copy Markdown
Contributor Author

update: referencing VRP report https://issuetracker.google.com/u/1/issues/522586712

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.

1 participant