Skip to content

nvx-rs: factor minimal guest rootfs and descriptor-rooted host mapping #539

Closed as not planned

Description

Parent: #535. Phase 2 (guest foundation). Build on #358, the open guest-tool PR #528 and managed-agent PR #531. #359 ports a host launch profile; it does not port the guest rootfs shell in guest/common/nvx-init-agent.

Goal

Extract a small, transport-neutral nvx::guest image/root/process substrate from the Rust guest port and the relevant shell init steps. A standalone image-backed sandbox and a richer caller should assemble the same root and apply the same ordered host-map semantics without pulling a full application RPC stack into a static guest library.

Mechanics

  • Accept ordered read-only layers supplied by a profile-specific image resolver: GPT p2+ ext4 for one profile, EROFS layers for another. Specify each layer's cold-plugged device role and source-bound fixture, including the GPT multi-partition case. For this image-backed profile, one read-only GPT disk supplies every p2+ lower: direct distro on cold boot or bound image0 after template restore. Its partitions are not additional image slots; an EROFS-layer profile may use other resolver/device arrangements without adding slots to this one. Assemble an overlay with Upper::Tmpfs or Upper::Ext4Scratch; do not force a scratch disk on the RAM-upper path or change the existing shell profile before its replacement is accepted.
  • Decode and validate the current nvx_maps=COUNT + ordered APP_MAPS table from Port the managed agent to Rust #531, not the obsolete nvx_map= wording in nvx-rs: replace the guest C programs with Rust binaries #358. Separate validated HostMap { source, target, read_only } semantics from literal NVXC frames: another authenticated adapter may carry the same table in its own framing.
  • Apply mappings by opening source and overlay root descriptors, resolving beneath them without symlink/magic-link escapes, creating targets only on the owned overlay, and applying RO/nosuid/nodev before attaching each bind. Reserve runtime-owned targets consistently; Track sandbox live share bugs: the time ABI bind hides or breaks shares at /run targets #486 tracks an existing /run/nvx/time share collision, so this core must not repeat it.
  • New activation profiles accept zero mappings (no batches/dormant export). Public NVXC v1 retains its existing 1..4096 boot-count and nonempty MAPS grammar for any explicitly supported old bundle. Nonzero tables require exact order/count before Ready/exec; partial side effects fail closed.
  • Expose basic root-entry/spawn/cancel/reap and whole-workload settlement hooks under a caller-fixed identity/isolation contract. A terminal exec must not leave a descendant or inherited pipe writer before readmission, even after cancellation/timeout/reconnect; failure blocks further exec or tears down the verified VM. Preserve caller-fixed non-root/cgroup/setpriv and bounded-capability-root policies without a user-facing isolation-profile selector or protobuf, ttrpc, OCI, runc, or host Tokio in the small guest feature. A root caller must not reach the raw export, control device or configuration region or change a read-only host mapping.

Acceptance

Activity

  1. ppenna commented on Oct 16, 2026

    @ppenna
    Contributor

    Five suggestions from the #531 review belong here. #531 keeps a faithful port of the C agent, so they wait until the #358 series is done:

    • Give launch neutral inputs (review). SpawnRequest carries AgentConfig, the agent's argv strings, and GuestExecPayload, the decoded NVXC frame. Convert them once at dispatch into a neutral exec spec (argv, environment, inherit flag, cwd, timeout) and an explicit identity, so that the root-entry and spawn hooks take input that another adapter can build. UID and GID validation can stay at launch time to keep today's failure stage.
    • Give reaping a single owner (review). cgroup::reap and cgroup::verify_empty call waitpid(-1, WNOHANG) from the execution path, as the C agent does, so they collect any child of the process. One agent-owned reaper that keeps PID 1's orphan reaping and records exit statuses by PID, which the execution core asks for its own child's status, lets the core run in a process with other children (nvx-rs: prove shared host and guest mechanics through two independent adapters #545).
    • Pass the execution cgroup in as a value (review). cgroup::prepare, has_tasks, kill, and launch.rs each rebuild the nvx-exec path, so only the direct root's fixed cgroup gets cgroup-level settlement, and in sandbox mode cgroup::terminate kills only the process group. An ExecCgroup { dir } with prepare, has_tasks, kill, settle, and procs serves whole-workload settlement in a layered root and one cgroup per execution for a concurrent adapter (nvx-rs: prove shared host and guest mechanics through two independent adapters #545). NVXC v1 still creates nvx-exec on every EXEC.
    • Make the mapping roots explicit, and keep NVXC batching separate (review). For example, apply_maps(system, export_root, target_root, first, maps). The NVXC loop keeps HostMappings and its sequence rules and passes /, so paths stay byte-identical until the descriptor-rooted walk replaces the inside.
    • Test symlinks in intermediate path components (review). Mapping application resolves the source and target by name: make_directories and both mount calls follow symlinks, and only the file target refuses one, with O_NOFOLLOW on its last component. The C agent does the same. For example, with work read-only, work/out read-write, and work/out/deps/cache read-only, a writer that replaces work/out/deps with a symlink to /var after the host's ExportPlan::verify makes the bind and its remount land on the guest's /var/cache. The descriptor-rooted walk (openat2 with RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS, then open_tree, mount_setattr, and move_mount) must refuse it, and a regression test should cover this case.

    The first four keep NVXC v1's wire bytes, limits, exit statuses, and diagnostics. #358 checks in the C agent's parity fixtures before it removes the C agent, and those fixtures can prove it.

  2. esaurez commented on Oct 16, 2026

    @esaurez
    ContributorAuthor

    From the #550 review (the Rust time tools), deferred because #550 keeps a faithful port. For this core:

    • Expose the time steps as library calls. In Port the time tools to Rust #550, nvx::guest::time and nvx::guest::reseed export only main(), and the validated restore packet leaves repair_restore only as a text line and an entropy file. A Rust PID 1 must therefore start nvx-time capture, nvx-reseed and nvx-time restore-finish and parse their output (#358 asked PR 4 for these calls). Keep the CLI as a thin wrapper, with today's bytes and statuses, over calls such as:

      • time::boot();
      • time::capture(request, previous_id), which returns the validated RestorePacket after the clock step, or that nothing was restored;
      • time::restore_finish(acknowledge, new_cpus);
      • reseed::reseed(seed, previous_id).

      Failures stay fatal through a helper that the CLI also uses. The time daemon stays a separate process, because C10 requires its name to be nvx-time and its PID to be greater than 1.

    • Scope port access to each transaction. RealOps requests PortAccess for the whole command, and the thread's I/O bitmap passes to fork children and survives execve, whatever their capabilities. An in-process caller must take and drop it for each port transaction, never while it starts a workload. Add a test that a workload can neither read the byte channel nor write the snapshot or shutdown port.

    With both, neither adapter needs nvx-snapshot or helper processes on a readiness path (#542, #545).

  3. esaurez commented on Oct 20, 2026

    @esaurez
    ContributorAuthor

    Replaced by the rewritten #535. Neutral launch, a single reaper, an exec cgroup, explicit roots, time as library calls and per-transaction ports move to #619 (A1-guest). The minimal layered root, with the Rust PID 1, descriptor-rooted mappings and ordered maps, moves to #621 (A3). Closing in favor of those issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions