Skip to content

fix(guest): take the zero-copy input via the safe get_private_input_slice - #898

Merged
MauroToscano merged 1 commit into
perf/guest-zerocopyfrom
fix/guest-zerocopy-safe-slice
Aug 4, 2026
Merged

fix(guest): take the zero-copy input via the safe get_private_input_slice#898
MauroToscano merged 1 commit into
perf/guest-zerocopyfrom
fix/guest-zerocopy-safe-slice

Conversation

@MauroToscano

Copy link
Copy Markdown
Contributor

Review fix on top of #886, targeting that branch so it lands inside it.

The zero-copy read is the right call — get_private_input() really does copy the
whole input for nothing. But syscalls::get_private_input_slice already borrows
the mapped region in place and returns &'static [u8], and get_private_input
is literally that call plus a to_vec(). So dropping to the slice is the entire
win, without the pointer plumbing:

let input = lambda_vm_syscalls::syscalls::get_private_input_slice();
let input = rkyv::from_bytes::<ProgramInput, Error>(input).unwrap();

Three things that buys beyond the line count:

  • No raw pointers in guest code. syscalls.rs keeps the region layout and its
    one unsafe block in a single place — the stated reason
    get_private_input_slice exists. Re-reading the length prefix in the guest
    duplicates layout knowledge that has to stay in step with the executor.
  • Restores the length-prefix clamp. get_private_input_slice bounds the prefix
    by MAX_PRIVATE_INPUT_SIZE; ef_io::read_input returns it raw. The executor
    rejects oversized inputs, so honest runs are byte-identical either way — but a
    forged prefix built a slice reaching past the region instead of a bounded one.
    Robustness, not soundness: private input is prover-chosen by definition.
  • Drops a dependency on unspecified behavior. ef_io::read_input documents
    buf_ptr as unspecified when buf_size == 0, and the previous code fed it to
    from_raw_parts regardless. Harmless in practice (the implementation always
    writes it, and ethrex input is never empty), but not a contract to lean on.

bench_vs/lambda/recursion already reads its blob exactly this way.

The measured win should be unchanged — both forms skip the same to_vec() — but
it is worth re-running the cycle counts from #886 to confirm, since that number
is the whole point of the PR.

Verified locally

  • make executor/program_artifacts/rust/ethrex.elf — builds clean.
  • cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block
    (CI's exact command) — 4 passed, 0 failed. test_ethrex executes the rebuilt
    guest ELF and compares its public output against a native execution_program
    run, so the changed read path is covered.
  • rustfmt --check clean.

Not re-measured: the cycle deltas, and proving-level coverage
(test_prove_ethrex_empty_block is #[ignore] and no CI job selects it).

…lice

The zero-copy read is the right call, but it hand-rolls what
`syscalls::get_private_input_slice` already does: borrow the mapped
private-input region in place and hand back `&'static [u8]`, no copy and no
allocation. `get_private_input` is that same call plus a `to_vec()`, so
dropping to the slice is the whole win without the pointer plumbing.

Three things that buys:

- No raw pointers in guest code. `syscalls.rs` deliberately keeps the region
  layout and its one `unsafe` block in a single place — that is why
  `get_private_input_slice` exists. Re-reading the length prefix in the guest
  duplicates layout knowledge that has to stay in step with the executor.
- Restores the length-prefix clamp. `get_private_input_slice` bounds the
  prefix by `MAX_PRIVATE_INPUT_SIZE`; `ef_io::read_input` returns it raw. The
  executor rejects oversized inputs, so honest runs are identical — but a
  forged prefix built a slice reaching past the region instead of a bounded
  one.
- Drops a dependency on unspecified behavior. `ef_io::read_input` documents
  `buf_ptr` as unspecified when `buf_size == 0`, and the previous code fed it
  to `from_raw_parts` regardless. Harmless in practice (the implementation
  always writes it, and ethrex input is never empty), but not a contract to
  lean on.

`bench_vs/lambda/recursion` already reads its blob this way.
@MauroToscano
MauroToscano merged commit a6ca32e into perf/guest-zerocopy Aug 4, 2026
14 checks passed
@MauroToscano
MauroToscano deleted the fix/guest-zerocopy-safe-slice branch August 4, 2026 19:28
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