feat: add memory bounds check for rvr#2803
Conversation
|
Claude finished @mansur20478's task in 1m 56s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Overall this is a clean, well-scoped PR. The bounds-checking logic matches the interpreter's Bug: OOB access still proceeds after detection
Semantic mismatch with interpreterThe interpreter has two modes (
The C side has:
So in protected mode the C version is weaker (log vs. panic), and in unprotected mode both are no-ops in release. This is fine as a draft, but the final version should match the interpreter's panic semantics (e.g., Nit:
|
This comment has been minimized.
This comment has been minimized.
|
Claude finished @mansur20478's task in 1m 41s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Good iteration from the first commit. The second commit ( Remaining items1.
2. At const size_t mem_size = MEMORY_MASK + 1u;
const size_t mem_size = (size_t)MEMORY_MASK + 1u;3. Minor: At if cfg!(feature = "unprotected") {
make_args.push("OPENVM_UNPROTECTED=1".to_string());
}
Looks good
Summary: This is ready to merge once the |
This comment has been minimized.
This comment has been minimized.
|
Claude finished @mansur20478's task in 1m 54s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Nice iteration across the three commits. The latest commit ( One minor itemUnused
Not a blocker either way — just noting for awareness. What looks good
Summary: This is clean and correct. The only nit is the unused |
This comment has been minimized.
This comment has been minimized.
|
Claude finished @mansur20478's task in 1m 51s —— View job Code Review: Memory Bounds Check for RVR (#2803)
This is a clean, well-structured PR. The bounds checking correctly mirrors the interpreter's One item worth addressing
Two options:
Not a blocker, just noting the tradeoff. Looks good
Summary: LGTM. The implementation is correct, well-structured, and faithfully mirrors the interpreter. The only observation (static |
| /* ── Per-width memory reads ──────────────────────────────────────── */ | ||
|
|
||
| static __attribute__((always_inline)) inline uint8_t rd_mem_u8(RvState* restrict state, uint32_t addr) { | ||
| #ifndef OPENVM_UNPROTECTED |
There was a problem hiding this comment.
better to have the ifndef inside check_mem_bounds and make it noop otherwise
but actually, let's just avoid ifndef altogether. we don't use any preprocessor directives in this code since they can make compilation slower. let's define an externally statically linked check_mem_bounds function that is noop for unprotected and does bounds checking for protected. we can have two separate header or c files
|
|
||
| static __attribute__((always_inline)) inline int8_t rd_mem_i8(RvState* restrict state, uint32_t addr) { | ||
| #ifndef OPENVM_UNPROTECTED | ||
| check_mem_bounds(state, addr, sizeof(int8_t)); |
There was a problem hiding this comment.
either inline checks or create separate check_mem_bounds_xx functions and just use
if (unlikely(start > mem_size - sizeof(int8_t))) abort_oob(...);
| @@ -135,12 +186,20 @@ static __attribute__((always_inline)) inline void wr_mem_u32(RvState* restrict s | |||
| * so these lower to a single memcpy. */ | |||
| static __attribute__((always_inline)) inline void rd_mem_u32_range(RvState* restrict state, uint32_t base_addr, | |||
There was a problem hiding this comment.
this probably is the only one that needs both checks in the if statement
| abort(); | ||
| } | ||
|
|
||
| static __attribute__((always_inline)) inline void check_mem_bounds(RvState* restrict state, uint32_t start, size_t size) { |
| const size_t mem_size = (size_t)MEMORY_MASK + 1u; | ||
| if (unlikely(start > mem_size || size > mem_size - start)) { | ||
| abort_oob(start, size, mem_size); | ||
| } |
There was a problem hiding this comment.
add a __builtin_assume((start & ~(uint32_t)MEMORY_MASK) == 0u); after if. maybe it helps
|
Claude finished @mansur20478's task in 3m 4s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Good iteration addressing the prior review feedback. The latest commit ( Issues to address1.
__builtin_assume((start & ~(uint32_t)MEMORY_MASK) == 0u);These assumptions live inside a separate translation unit. Since Either:
Actually, looking again: 2. Missing trailing newlines
3. Unnecessary
4. Even if you keep the Minor observations (non-blocking)Duplicated Semantic gap with interpreter in unprotected mode — The interpreter's unprotected What looks good
Summary: The refactor to separate |
This comment has been minimized.
This comment has been minimized.
|
can you also add a test somewhere under |
gdmlcjs
left a comment
There was a problem hiding this comment.
I think you should also add checking for the public values address space.
| let bounds_prot_path = self | ||
| .output_dir | ||
| .join("openvm_check_mem_bounds_protected.c"); | ||
| fs::write( | ||
| &bounds_prot_path, | ||
| include_str!("../../c/openvm_check_mem_bounds_protected.c"), | ||
| )?; | ||
| let bounds_unprot_path = self | ||
| .output_dir | ||
| .join("openvm_check_mem_bounds_unprotected.c"); | ||
| fs::write( | ||
| &bounds_unprot_path, | ||
| include_str!("../../c/openvm_check_mem_bounds_unprotected.c"), | ||
| )?; |
There was a problem hiding this comment.
I think it would be better if we did the specialization in this file like how it's done for the tracer header. We can do that with a single #[cfg(feature = "unprotected")] branch instead of passing an argument to the Makefile.
|
Claude finished @mansur20478's task in 3m 8s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Good iteration across the 5 commits. The latest state ( Issues to address1. Missing trailing newline in The file ends with 2. Every accessor in check_mem_bounds_u32(addr);
__builtin_assume((addr & ~(uint32_t)MEMORY_MASK) == 0u);In protected mode, In unprotected mode, the stubs are no-ops, so Recommendation: remove the 3. if (unlikely(base_addr > mem_size || size > mem_size - base_addr))When 4. Duplicated
Not blocking. Reviewer feedback status
What looks good
Summary: The implementation is correct and well-structured. Main items: (1) add trailing newline to |
This comment has been minimized.
This comment has been minimized.
|
Claude finished @mansur20478's task in 2m 6s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Good iteration across the 6 commits. The architecture is clean: header declares per-width functions, Issues to address1. Every accessor ( check_mem_bounds_u32(addr);
__builtin_assume((addr & ~(uint32_t)MEMORY_MASK) == 0u);
// ... mem_ptr(state, addr) ...But Additionally, in unprotected mode, the check is a no-op, so 2. Test doesn't verify the child actually failed
let output = Command::new(std::env::current_exe().unwrap())
.args(["--exact", "tests::test_out_of_bound_mem_access", "--nocapture"])
.env("OPENVM_OOB_CHILD", "1")
.output()
.expect("failed to spawn self");
panic!("{}", String::from_utf8_lossy(&output.stderr));If the child somehow succeeds (e.g., the OOB address lands within bounds due to a config change, or the bounds check is accidentally compiled as a no-op), the test would still panic with whatever noise is on stderr and if output.status.success() {
panic!("child process succeeded — OOB access was not caught");
}This makes the test assertion tighter and prevents false passes. 3. Duplicated
Not blocking, but worth a note. 4. Missing trailing newline in
Reviewer feedback status
What looks good
Summary: The implementation is correct and well-structured. Main items: (1) remove the redundant |
This comment has been minimized.
This comment has been minimized.
|
Claude finished @mansur20478's task in 2m 31s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Good iteration across 7 commits. The latest commit ( Bug: Missing guest program
|
| Reviewer | Feedback | Status |
|---|---|---|
| shuklaayush | Separate .c files instead of #ifndef |
✅ Addressed in 7a7aaac |
| shuklaayush | Per-width check_mem_bounds_* functions |
✅ Addressed |
| shuklaayush | Remove state parameter |
✅ Addressed |
| shuklaayush | Add __builtin_assume after check |
✅ Added (see note above about redundancy) |
| shuklaayush | should_panic test under feature = "rvr" |
✅ Added in 14cc871 + 33fc9c9 |
| gdmlcjs | Specialization in project.rs like tracer header |
✅ Addressed in 9a3f170 |
| gdmlcjs | Bounds checking for public values address space | ✅ Addressed in 33fc9c9 — host_print_str, host_hint_storew, host_hint_buffer now call check_mem_bounds_range. host_reveal already had an assert! for public_values bounds. |
What looks good
- Bounds check logic:
start > mem_size - sizeof(T)for fixed-width types is the correct single-comparison form. The range variant (protected.c:53) correctly uses the two-check formbase_addr > mem_size || size > mem_size - base_addrmatching the interpreter atmemmap.rs:41. - Cold-path split:
abort_oobisnoinline, cold, noreturn— keeps the hot path tight. Correctly mirrors#[cold] #[inline(never)] fn panic_oobinmemmap.rs. (size_t)MEMORY_MASK + 1u: Defensive cast prevents 32-bit overflow.- Feature-flag chain:
openvm-cli → openvm-sdk → openvm-circuit → rvr-openvmviaunprotected = ["rvr-openvm/unprotected"].project.rsuses#[cfg(feature = "unprotected")]to select the.cfile at build time — clean and follows the tracer header pattern. - Rust-side host callback bounds checking (
io.rs:43-63): Clean dual implementation with#[cfg]/#[cfg(not)]for protected/unprotected. Correctly checkshost_print_str,host_hint_storew, andhost_hint_buffer. - Test design: The parent/child process pattern is a pragmatic solution for testing
abort()from C code. Theblack_boxusage correctly prevents the compiler from optimizing away the OOB access. - Benchmarks: No measurable regression across all benchmarks.
Summary: The implementation is correct and well-structured. One blocking item: the missing out_of_bound_print_str guest program means test_out_of_bound_print_str won't compile. Two non-blocking items: the redundant __builtin_assume lines can be cleaned up, and the duplicated unlikely function is a minor maintenance hazard.
|
Claude finished @mansur20478's task in 2m 37s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Clean PR with good iteration across 8 commits. The architecture is well-structured: header declares per-width Items to address1. Every accessor in check_mem_bounds_u32(addr);
__builtin_assume((addr & ~(uint32_t)MEMORY_MASK) == 0u);But Additionally, in unprotected mode, the check is a no-op so Recommendation: remove all 10 2. Duplicated
3. Missing trailing newline in
Minor observations (non-blocking)
Semantic gap with interpreter in unprotected mode: The interpreter's unprotected Reviewer feedback status
What looks good
Summary: The implementation is correct and well-structured, faithfully mirroring the interpreter's bounds-checking semantics. The main cleanup item is the redundant |
This comment has been minimized.
This comment has been minimized.
|
Claude finished @mansur20478's task in 2m 32s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Clean PR with good iteration across 9 commits. The architecture is well-structured: a header declares per-width 1.
|
| Reviewer | Feedback | Status |
|---|---|---|
| shuklaayush | Separate .c files instead of #ifndef |
✅ Addressed |
| shuklaayush | Per-width check_mem_bounds_* functions |
✅ Addressed |
| shuklaayush | Remove state parameter |
✅ Addressed |
| shuklaayush | Add __builtin_assume after check |
✅ Added (see note above about redundancy) |
| shuklaayush | should_panic test under feature = "rvr" |
✅ Added — both C-side OOB and Rust-side hostcall OOB |
| gdmlcjs | Specialization in project.rs like tracer header |
✅ Addressed via #[cfg] on include_str! |
| gdmlcjs | Bounds checking for public values address space | ✅ Addressed — host_print_str, host_hint_storew, host_hint_buffer now call check_mem_bounds_range |
What looks good
- Bounds check logic:
start > mem_size - sizeof(T)for fixed-width types is the correct single-comparison form. The range variant correctly uses the two-check formbase_addr > mem_size || size > mem_size - base_addr, avoiding overflow. - Cold-path split:
abort_oobisnoinline, cold, noreturn— keeps the hot path tight. Correctly mirrors the interpreter's#[cold] #[inline(never)] fn panic_oobpattern. (size_t)MEMORY_MASK + 1u: Defensive cast prevents 32-bit overflow. WithMEM_BITS=29this was safe anyway, but the cast future-proofs it.- Feature-flag chain:
openvm-circuit → rvr-openvmviaunprotected = ["rvr-openvm/unprotected"].project.rsuses#[cfg(feature = "unprotected")]to select the.cfile at build time — clean and follows the tracer header pattern. - Rust-side host callback bounds checking (
io.rs:43-63): Clean dual implementation with#[cfg]/#[cfg(not)]. Correctly checkshost_print_str,host_hint_storew, andhost_hint_buffer. The check condition exactly matches the interpreter's. - Test design: The parent/child process pattern is a pragmatic solution for testing
abort()from C code. Both tests correctly verify the child failed (output.status.success()check).black_boxprevents the compiler from optimizing away the OOB access. Both C-side OOB (mem access) and Rust-side OOB (print_str hostcall) are covered. - All files have trailing newlines — the prior review's concern about missing newlines in
.h/.c/Cargo.tomlhas been addressed. - No Makefile changes needed: The
SRCS = $(wildcard *.c)picks up whichever.cfileproject.rsemits. - Benchmarks: No measurable regression.
Summary: The implementation is correct and well-structured, faithfully mirroring the interpreter's bounds-checking semantics. The main cleanup item is the redundant __builtin_assume in every accessor (item 1). The duplicated unlikely (item 2) and MEM_BITS source divergence (item 4) are minor. All reviewer feedback has been addressed. Nice work.
This comment has been minimized.
This comment has been minimized.
|
Claude finished @mansur20478's task in 3m 3s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Clean PR with good iteration across 10 commits. The architecture is well-structured: a header ( 1.
|
| Reviewer | Feedback | Status |
|---|---|---|
| shuklaayush | Separate .c files instead of #ifndef |
✅ Addressed |
| shuklaayush | Per-width check_mem_bounds_* functions |
✅ Addressed |
| shuklaayush | Remove state parameter |
✅ Addressed |
| shuklaayush | Add __builtin_assume after check |
✅ Added (see note above about redundancy with mem_ptr) |
| shuklaayush | should_panic test under feature = "rvr" |
✅ Added — both C-side OOB and Rust-side hostcall OOB |
| gdmlcjs | Specialization in project.rs like tracer header |
✅ Addressed via #[cfg] on include_str! |
| gdmlcjs | Bounds checking for public values address space | ✅ Addressed — host_print_str, host_hint_storew, host_hint_buffer now call check_mem_bounds_range. host_reveal already had an assert!. |
What looks good
- Bounds check logic:
start > mem_size - sizeof(T)for fixed-width types is the correct single-comparison form. The range variant (protected.c:53) correctly uses the two-check formbase_addr > mem_size || size > mem_size - base_addr, matching the interpreter. - Cold-path split:
abort_oobisnoinline, cold, noreturn— keeps the hot path tight. Correctly mirrors the interpreter's#[cold] #[inline(never)] fn panic_oobpattern. (size_t)MEMORY_MASK + 1u(protected.c:21): Defensive cast prevents 32-bit overflow. WithMEM_BITS=29(MEMORY_MASK=0x1FFFFFFF) this was safe anyway, but the cast future-proofs it.- Feature-flag chain:
openvm-cli → openvm-sdk → openvm-circuit → rvr-openvmviaunprotected = [...]at each level.project.rsuses#[cfg(feature = "unprotected")]to select the.cfile at build time — clean and follows the tracer header pattern. - Rust-side host callback bounds checking (
io.rs:43-63): Clean dual implementation with#[cfg]/#[cfg(not)]. Correctly checkshost_print_str,host_hint_storew, andhost_hint_buffer. The check condition exactly matches the interpreter's. - Test design: The parent/child process pattern is a pragmatic solution for testing
abort()from C code. Both tests correctly verify the child failed (output.status.success()check).black_boxprevents the compiler from optimizing away the OOB access. Both C-side OOB (mem access) and Rust-side OOB (print_str hostcall) paths are covered. - No Makefile changes needed: The
SRCS = $(wildcard *.c)picks up whichever.cfileproject.rsemits. - Trailing newlines: All files now have proper trailing newlines (fixed from prior review feedback).
- Benchmarks: No measurable regression across all benchmarks.
Summary: The implementation is correct and well-structured, faithfully mirroring the interpreter's bounds-checking semantics. The main cleanup item is the redundant __builtin_assume in every accessor (item 1). The duplicated unlikely (item 2) and the cross-TU inlining limitation (item 3) are minor observations. All reviewer feedback has been addressed. Nice work iterating on the review feedback.
This comment has been minimized.
This comment has been minimized.
gdmlcjs
left a comment
There was a problem hiding this comment.
LGTM! Only the lint failure seems like a problem
|
Claude finished @mansur20478's task in 2m 35s —— View job Code Review: Memory Bounds Check for RVR (#2803)
Clean PR with good iteration across 12 commits. The architecture is well-structured: a header declares per-width 1.
|
Note: cells_used metrics omitted because CUDA tracegen does not expose unpadded trace heights. Commit: 69da03b |
Memory read and write functions now have an optional
check_boundsinvocation before accessing the memory.check_boundschecks that the access lies within the VM's addressable memory region and aborts otherwise. The same is applied foropenvm_io.hfunctions that work with the user IO address space in data memory.To turn off protected mode, add the
openvm-cli/unprotectedfeature.Mirrors the interpreter's
check_boundsandpanic_oobfunctions, andunprotectedCargo feature.closes INT-7702