Skip to content

Fix integer-overflow bypass of bounds check in get_safeptr()#11

Merged
ringtailsoftware merged 1 commit into
ringtailsoftware:mainfrom
94xhn:fix/get-safeptr-integer-overflow
Jul 16, 2026
Merged

Fix integer-overflow bypass of bounds check in get_safeptr()#11
ringtailsoftware merged 1 commit into
ringtailsoftware:mainfrom
94xhn:fix/get-safeptr-integer-overflow

Conversation

@94xhn

@94xhn 94xhn commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

get_safeptr() is the bounds-checking primitive behind uvm32_arg_getslice() / uvm32_arg_getslice_fixed(), which the docs describe as the way a host should safely turn a (ptr, len) syscall argument pair supplied by VM bytecode into a slice. Both addr and len come directly from VM registers a0/a1, so they are fully attacker-controlled if the running bytecode is untrusted — which is the whole point of the library ("bad code running in the VM should never be able to crash the host").

The guard is:

if ((ptrstart > limit) || (ptrstart + len > limit)) { ... reject ... }

ptrstart + len is computed in uint32_t. If len is chosen so the sum wraps past 2^32 (e.g. ptrstart = 1, len = 0xFFFFFFFF), the wrapped sum lands back at/below limit, so the check is bypassed and the function returns a slice whose declared len is far larger than the real buffer. Any host code that trusts that len for a subsequent copy/read/write will overrun its buffer.

I verified this by compiling the unmodified uvm32/uvm32.c (UVM32_MEMORY_SIZE=64) and calling only the public API: setting regs[10]/regs[11] (a0/a1) to addr = RAM_IMAGE_OFFSET+1 and len = 0xFFFFFFFF, wiring _params[0]/[1] to those registers the same way uvm32_run() does for a real syscall, then calling uvm32_arg_getslice(). It returns a valid-looking pointer with len == 0xFFFFFFFF against a real 64-byte memory buffer. The same happens on the extram path.

Fix: compare len against limit - ptrstart after first confirming ptrstart <= limit (which cannot overflow), instead of comparing the (overflow-prone) sum ptrstart + len against limit. Applied to both the main-memory branch and the extram branch.

Added test_syscall_args_bufrd_overflow_bypass in test/syscall_args/test/tests.c, following the existing style in that file: it triggers the existing SYSCALL_C path (real, in-bounds pointer from the rom), overrides ARG1 to 0xFFFFFFFF to simulate attacker-controlled bytecode, and asserts uvm32_arg_getslice() now rejects it (len == 0, subsequent run ends in UVM32_ERR_MEM_RD) rather than silently overflowing.

I don't have a RISC-V cross-compiler / working Docker daemon in my local environment to run the full make ci suite end-to-end here, but the added test mirrors the exact structure of the neighboring test_syscall_args_bufrd_toolarge/test_syscall_args_bufrd_bad_addr tests, and I separately confirmed with a standalone harness (compiling the exact same get_safeptr() logic) that the fix preserves every existing boundary case (exact-fit read, one-byte-over rejection, in-bounds slice, too-large length, bad address, zero-length-at-boundary) while closing the overflow bypass.

get_safeptr() is the bounds-checking primitive behind
uvm32_arg_getslice()/uvm32_arg_getslice_fixed(), used by a host to safely
turn a (ptr, len) syscall argument pair supplied by VM bytecode into a
slice. Both addr and len come directly from VM registers a0/a1, so are
fully attacker-controlled if the running bytecode is untrusted.

The guard computed ptrstart + len in uint32_t and compared the sum
against the memory limit. If len is chosen so that ptrstart + len wraps
past 2^32 (e.g. ptrstart=1, len=0xFFFFFFFF), the wrapped sum can land
back at or below the limit, bypassing the check while returning a slice
whose declared len vastly exceeds the real buffer. Any host code that
subsequently trusts that len for a copy/read/write will overrun its
buffer.

Fix by comparing len against (limit - ptrstart) after confirming
ptrstart <= limit, which cannot overflow. Applies to both the main
memory path and the extram path.

Added a regression test (test_syscall_args_bufrd_overflow_bypass) that
requests a slice with a real in-bounds pointer but len=0xFFFFFFFF and
asserts it is now rejected.
@94xhn
94xhn temporarily deployed to github-pages July 16, 2026 08:55 — with GitHub Actions Inactive
@ringtailsoftware
ringtailsoftware merged commit a5b1e03 into ringtailsoftware:main Jul 16, 2026
1 check passed
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.

2 participants