Skip to content

rvvm_user: standalone userland runner with Linux syscall layer hardening - #288

Merged
LekKit merged 5 commits into
LekKit:stagingfrom
linsmod:userland-hardening
Aug 24, 2026
Merged

rvvm_user: standalone userland runner with Linux syscall layer hardening#288
LekKit merged 5 commits into
LekKit:stagingfrom
linsmod:userland-hardening

Conversation

@linsmod

@linsmod linsmod commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Motivation

Upstream ships rvvm_user.c as dormant experimental code: RVVM_USER_TEST is
commented out and there is no build target for it. This PR makes the userland
runner actually usable as a standalone rvvm_user binary, and hardens the
Linux syscall layer until real-world static RISC-V binaries run to completion.

For validation this PR borrows guest ELFs from a reproduction package aimed at
user-mode host implementations of a guest-JIT bug: a statically linked
riscv64 musl JS runtime ("ant", cross-compiled with zig) plus a 1MB JS
workload. They are used here solely to verify that rvvm_user runs
real-world static binaries.

Summary

A standalone userland runner (rvvm_user) plus a set of Linux syscall-layer
fixes discovered while running real-world static RISC-V binaries under
user-mode emulation.

Changes

New entry point / build

  • src/rvvm_user_main.c: standalone rvvm_user binary entry
  • project.mk: builds rvvm_user and defines RVVM_USER_TEST only on
    64-bit Linux targets; all other targets (including i386) keep the existing
    upstream stub behavior unchanged

Syscall correctness fixes

  • 425 io_uring_setup: return -ENOSYS instead of falling through
  • 123 sched_getaffinity: pass through the host affinity mask
  • 21/22 epoll_ctl / epoll_pwait: translate between the packed x86-64 host
    struct epoll_event (12 bytes) and the naturally-aligned guest layout
    (16 bytes); previously events were corrupted on x86-64 hosts
  • 259 riscv_flush_icache: optional RVVM_JITDUMP=1 dumps each flushed
    guest code range (useful for debugging guest JITs)
  • 220 clone: RVVM_USER_NO_THREADS=1 knob forcing -EAGAIN (debug aid)

Usability

  • RVVM_USER_PREFIX env var overrides the hardcoded prefix_path (empty
    value disables the prefix); the hardcoded default silently broke absolute
    guest paths
  • RVVM_NOJIT env var for pure-interpreter userland (bisecting JIT issues)
  • ELF load failure diagnostics: open retries, distinct open vs VMA-collision
    errors, /proc/self/maps dump

ELF loading

  • Map 256MB (ELF_USERLAND_HEAP_MARGIN) past the fixed ELF image as the
    initial brk area, matching how real kernels map pages after the image;
    without this, early brk accesses on non-relocatable static ELFs fault

CI / build hygiene

  • prefix_path const-correctness and getcwd/chdir return value checks
    (-Werror clean)
  • Silenced GCC -fanalyzer fd-leak false positives: guest fd numbers passed
    through the syscall dispatch are not host descriptors (scoped #pragma,
    GCC 12+ only)

Testing

  • make CFLAGS="-Werror" clean on x86_64 GCC/clang, riscv64 cross-compile,
    and a mingw cross-build simulation; make test (riscv-tests) passes
  • Smoke-tested with a static riscv64 hello-world (exit code pass-through OK)
  • Validated with the borrowed ELF payload with a 1MB JS bundle:
    • https://github.com/linsmod/ant_agent_repro/releases/download/binaries-v1/ant_jit_runner_repro.zip

- Add src/rvvm_user_main.c entry point and rvvm_user build target
  behind the RVVM_USER_TEST flag (project.mk)
- syscall fixes for running real static ELF binaries (musl/ant runtime):
  * 425 io_uring_setup: return -ENOSYS instead of falling through
  * 123 sched_getaffinity: pass through host affinity mask
  * 21/22 epoll_ctl/epoll_pwait: convert between packed x86-64 host
    struct epoll_event (12B) and naturally-aligned guest layout (16B)
  * 259 riscv_flush_icache: optional RVVM_JITDUMP=1 dumps each flushed
    guest code range for inspection
  * 220 clone: RVVM_USER_NO_THREADS=1 forces -EAGAIN (debug knob)
- Allow prefix_path override via RVVM_USER_PREFIX env var (empty value
  disables the prefix entirely); the hardcoded default broke absolute
  guest paths
- Improve ELF load failure diagnostics: retry open, distinguish open vs
  VMA-collision failures, dump /proc/self/maps on load failure
- rvvm_create_userland: RVVM_NOJIT env var disables host JIT
  (pure interpreter mode for bisecting)
- elf_load: map ELF_USERLAND_HEAP_MARGIN (256MB) past the fixed ELF
  image so early brk accesses do not fault, matching kernel behavior

Verified with a 67MB static musl RISC-V ELF embedding the ant JS
runtime: boots, runs the event loop and produces output identical to
qemu-riscv64.
@LekKit
LekKit merged commit e8fa675 into LekKit:staging Aug 24, 2026
15 checks 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