Skip to content

nvx-rs slimming 2.2.2: a VM stops only with a proved exit, and can be reattached #1100

Closed

Description

Parent: #1080, in #1076. Needs: #1099. Unblocks: #1102, #1108, #1109, #1118.

Goal. A VM stops only with a confirmed exit, and its outcome report can be read; a caller that restarts can prove that a VM exited; a host connects to a VM and checks who serves the endpoint; and a running VM can be verified and reattached from values that the caller saved.

Notes.

  • In-place changes update their callers. Process::wait and report::read change their signatures in place; a crate-private wait keeps the old wrappers' behavior until their callers go in phase 4.

PRs, in order. Each PR has one goal, leaves dev green, and names in its description each old test of its area as kept, rewritten or retired, with each change that check_test_changes.py reports. Its tests follow the repository's test layout: a module's unit tests, and the code that only they use, sit in its tests.rs or tests/ and move with it. The estimates count core lines (code, not comments, docs or tests) and counted files; each PR records its measured values. Each PR does what its Scope says and no more: a case, input or layout that its Scope doesn't name, and whatever "Not in this PR" lists, is out of scope. Address every review comment within the scope, and ask before acting on one that asks for more. A PR whose measured core lines pass the stop that its Estimate names (twice the estimate, at most 600) stops and asks before continuing.

  1. SL2.9a: The old lifecycle wrappers keep a stable internal wait.

    • Objective: Process::wait is about to return exit proof instead of a boolean, but four legacy lifecycle callers still need the old answer. A crate-private compatibility helper isolates those callers so the public signature can change without broad lifecycle edits.
    • Contents: A crate-private Process::wait_bool; the four old callers (supervisor/mod.rs, store/vmm.rs, template/clone/restore.rs, template/store/capture.rs) use it, and the public wait delegates to it, so SL2.10 changes wait without editing them. It goes with its last caller.
    • Scope:
      • Process::wait_bool, crate-private in nvx-rs/src/host/process.rs, containing today's boolean wait behavior.
      • The four legacy callers in nvx-rs/src/host/supervisor/vmm.rs, nvx-rs/src/host/store/vmm.rs, nvx-rs/src/host/template/clone/restore.rs and nvx-rs/src/host/template/store/capture.rs switch to wait_bool.
      • The public Process::wait delegates to wait_bool until SL2.10 changes its result.
    • Not in this PR:
      • New exit-proof types or a public wait-signature change: SL2.10 adds both.
      • Changes to deadlines, polling intervals, pidfd behavior or Windows process-handle waits: behavior stays identical.
      • A generic compatibility trait or wrapper layer: one crate-private method serves four known callers.
      • Migrating or redesigning the legacy lifecycle wrappers: later removal takes wait_bool with its last caller.
    • Approach: Extract the existing body of Process::wait into one crate-private wait_bool, make the public method delegate, and update exactly the four named callers. Do not duplicate platform waits, add an abstraction, or alter error and timeout behavior.
    • Done when:
      • The four named legacy callers use wait_bool and retain their current boolean behavior.
      • Public Process::wait still passes the existing deadline, exit-event and polling tests through its delegation.
      • No other caller or lifecycle behavior changes in this compatibility PR.
    • Needs: SL2.9, in nvx-rs slimming 2.2.1: a VM is created in a private directory, with its restore-ready listener, then spawned #1099.
    • Estimate: 60 core lines, 5 counted files. Past 120 core lines, stop and ask before continuing.
  2. SL2.10: A VM stops only with a confirmed exit, and its outcome can be read.

    • Objective: Callers must not treat a kill request or elapsed deadline as proof that a VM stopped. The new stop result is returned only after a confirmed exit, preserves whether the VM exited itself or was forced, and carries a missing, valid or erroneous outcome-report read separately.
    • Contents: host::vm::stop(&Process, &VmOutputs, graceful, grace, deadline) → Stopped { self_exited, forced, report }, and an error of the new kind ErrorKind::StillRunning (new) when the exit isn't confirmed; Process::wait(deadline) → Option<Exited>; report::read → Result<Option<Report>>, where an unreadable or malformed report is an Io error. The old wrappers keep their behavior until their callers move.
    • Scope:
      • New host::vm::stop(&Process, &VmOutputs, graceful, grace, deadline) and Stopped { self_exited, forced, report }.
      • Process::wait(deadline) now returns Option<Exited>: proof after exit, or None when the deadline passes first.
      • host::report::read changes in place to Result<Option<Report>>: missing is Ok(None), while an unreadable or malformed report is an Io error.
      • Stop runs the supplied graceful step, waits until grace, forces through the verified process if needed, and waits until deadline; an unconfirmed exit is an error of the new kind ErrorKind::StillRunning.
      • The old supervisor, store, clone and capture wrappers keep their behavior through wait_bool.
    • Not in this PR:
      • Proving an exit after the caller restarts: SL2.10b changes Process::open for that case.
      • A background reaper, signal service, restart manager or unbounded wait: the caller supplies two deadlines.
      • Changing the legacy lifecycle wrappers' public behavior or report handling: their migration comes later.
      • Interpreting free-form outcome fields or inventing a report schema: Report keeps today's summary.
      • Treating a PID, kill success or timeout as exit proof: only the verified process wait produces Exited.
    • Approach: Build stop from the existing verified Process::{wait, kill} path and the existing report summarizer. Represent exit proof as a small value produced only by a successful verified wait. Read the report after exit and store its result inside Stopped, so a report error never hides the confirmed exit. Keep the old wrappers on wait_bool rather than changing them here. Add StillRunning to ErrorKind, with its spelling and its code.
    • Done when:
      • stop returns Stopped only after a confirmed exit and reports whether it self-exited or was forced.
      • stop fails with kind StillRunning when the deadline passes without confirmed exit.
      • An unreadable or malformed outcome is an Io error in Stopped::report, and a missing report is Ok(None).
      • Process::wait returns None at its deadline and an Exited proof after the process exits.
    • Needs: SL2.9a, above.
    • Estimate: 210 core lines, 5 counted files. Past 420 core lines, stop and ask before continuing.
  3. SL2.10b: A caller that restarts can prove that a VM exited.

    • Objective: After its own restart, a caller must distinguish a still-running recorded VM from one that definitely exited without trusting a reused process ID. Process::open returns either a verified running process or exit proof based on the recorded boot, ID and start time.
    • Contents: Process::open(&ProcessRef) → Opened::{Running(Process), Exited(Exited)} (new): Exited when no process of that ID and start time runs on this boot, or the boot has changed; never from the ID alone. Its old callers follow: nvx-rs/src/host/store/vmm.rs, nvx-rs/src/host/store/locked.rs and nvx-rs/src/host/supervisor/reattach.rs.
    • Scope:
      • Process::open(&ProcessRef) changes to Opened::{Running(Process), Exited(Exited)} in nvx-rs/src/host/process.rs.
      • Exited is returned when the recorded boot differs, or no process with both the recorded ID and start time runs on the current boot.
      • The existing callers in host/store/vmm.rs, host/store/locked.rs and host/supervisor/reattach.rs handle Opened without weakening their process checks.
      • Linux uses the existing boot ID, /proc start time and pidfd or polling path; Windows uses its boot ID, creation time and process handle.
    • Not in this PR:
      • Exit proof from a process ID alone: IDs can be reused.
      • Process enumeration, ancestry checks, command-line matching or a general process database: the recorded identity already has the required three fields.
      • Persisting ProcessRef, capture context or VM records: callers own their durable state.
      • Changing stop, reattach policy or interrupted-launch recovery beyond adapting the three existing callers.
      • Recovering an exit status from a process the caller did not retain: Exited proves exit, not how it exited.
    • Approach: Reuse the current open_reference checks and replace None with the explicit Opened::Exited result; a matched handle becomes Opened::Running. Keep permission and indeterminate host errors as errors. Update only the three callers that currently branch on Option, without adding scans, caches or fallback identity heuristics.
    • Done when:
      • Process::open returns Exited for another boot, a missing ID or the same ID with another start time.
      • Process::open returns Running only for a process matching the recorded boot, ID and start time.
      • Tests show that ID reuse alone never proves either identity, and indeterminate host checks remain errors.
      • The store and supervisor callers preserve their existing recovery behavior with the new result.
    • Needs: SL2.10, above.
    • Estimate: 80 core lines, 4 counted files. Past 160 core lines, stop and ask before continuing.
  4. SL2.11a: A host connects to a VM, and checks who serves the endpoint.

    • Objective: A host connection must verify that the recorded VM process serves its endpoint before authentication begins. The retained transport and NVXS connection APIs move into one public host module that returns the crate's error, while the old session connection remains as a temporary wrapper.
    • Contents: host::transport, whose functions return the crate's Result where host::session's SessionError and HostConnectionError were, each case with its kind and source: Transport: Send, with timed read and write, and unread, which dev added: what the peer hasn't read yet, where the platform can tell, moved from nvx-rs/src/host/platform/mod.rs; connect_verified(endpoint, &ProcessRef, deadline) -> Result<Box<dyn Transport>>, from connect in nvx-rs/src/host/session.rs, which retries until the endpoint exists and checks that the VM's process serves it; and HostConnection (attach, send, receive, identity, session), from nvx-rs/src/host/nvxs.rs. The old connect stays a wrapper until its callers move.
    • Scope:
      • New host::transport, which returns the crate's Result where host::session's SessionError and HostConnectionError were: a deadline is TimedOut, an exited VM Exited, a closed, reset, refused or broken session Protocol, and a transport failure Io, each with its source.
      • Transport: Send with timed read and write, plus unread where the platform can report bytes the peer has not read, moved from host/platform/mod.rs.
      • connect_verified(endpoint, &ProcessRef, deadline), adapted from host/session.rs, retries until the endpoint exists or the deadline passes and verifies its server against the VM process.
      • Linux verifies the Unix-socket peer process; Windows verifies the named-pipe server process.
      • HostConnection::{attach, send, receive, identity, session} moves from host/nvxs.rs, while the old connect delegates until its callers move.
    • Not in this PR:
      • A public endpoint-owner lookup that returns arbitrary process information: callers do not need one.
      • TCP, TLS, remote hosts, pluggable transports or a general networking framework: supported hosts use Unix sockets or Windows named pipes.
      • An asynchronous transport trait or executor integration: methods remain blocking with deadlines.
      • Changing the NVXS protocol, application messages or control-session behavior: this PR moves and unifies the retained host connection.
      • VM persistence, process discovery or reattach policy: SL2.11b composes this connection with saved values.
    • Approach: Move the existing platform transport trait and HostConnection implementation, then adapt the existing retrying session::connect into connect_verified using ProcessRef. Preserve Unix peer credentials, Windows server-PID checks and the current deadline behavior. Map the two old error enums' cases onto the crate's error kinds, keeping each source, instead of layering wrappers or defining a new I/O framework.
    • Done when:
      • connect_verified retries a not-yet-created endpoint within its deadline and connects when the recorded VM process serves it.
      • Linux and Windows tests refuse an endpoint served by another process.
      • HostConnection retains attach, send, receive, identity and session behavior, and each failure has the kind and source that the scope names.
      • Transport::unread keeps its Linux result and Windows None behavior, with the old connect still serving unmoved callers.
    • Needs: SL2.9, in nvx-rs slimming 2.2.1: a VM is created in a private directory, with its restore-ready listener, then spawned #1099.
    • Estimate: 250 core lines, 5 counted files. Past 500 core lines, stop and ask before continuing.
  5. SL2.11b: A running VM can be verified and reattached from values that the caller saved.

    • Objective: A caller that saved a VM's launch values needs to verify or reattach it after restarting without a library-owned store. Verification checks the runtime and process without connecting; reattach then peer-verifies the endpoint and authenticates, while async callers can obtain an owned exit-wait handle where the host supports one.
    • Contents: host::vm::SavedVm { vm_dir, endpoint, capability, process, runtime }, which the caller saves; verify(&SavedVm, &RuntimeIdentity) checks the runtime identity and the process without connecting, and another runtime is an error of kind RuntimeMismatch; reattach(&SavedVm, &Runtime, deadline) verifies, then connects and attaches, and returns Reattached { process, connection }. Process::exit_handle() returns Option<ExitHandle> (new): a duplicated pidfd or process handle for an async runtime, or None when the host only polls. Process::bytes_read and host::process::own_bytes_read, which dev added for restore checks, stay public, with their platform rules.
    • Scope:
      • host::vm::SavedVm { vm_dir, endpoint, capability, process, runtime }, plus verify, reattach and Reattached { process, connection }.
      • verify(&SavedVm, &RuntimeIdentity) compares the saved runtime and opens the verified process without connecting to the endpoint; another runtime is an error of the kind RuntimeMismatch.
      • reattach(&SavedVm, &Runtime, deadline) verifies, calls connect_verified, attaches with the saved capability and returns the held process and HostConnection.
      • Process::exit_handle() -> Option<ExitHandle> and the new owned ExitHandle: a duplicated pidfd on supporting Linux hosts, a duplicated process handle on Windows, or None on a Linux host that only polls.
      • Process::bytes_read and host::process::own_bytes_read remain public with today's Linux and Windows accounting rules.
    • Not in this PR:
      • Serializing, encrypting, journaling or locating SavedVm: the caller saves and reloads its values.
      • A library store, VM registry, recovery daemon or interrupted-launch policy: callers build those policies.
      • Hashing runtime files or accepting a different runtime identity: the caller supplies and compares the existing labels and contract versions.
      • An asynchronous runtime adapter, future, watcher thread or callback API: callers adopt the duplicated exit handle or poll.
      • Reattaching to a mismatched runtime or unverified endpoint: such a VM may only be handled through its verified process.
    • Approach: Make SavedVm a value bundle, then compose the existing runtime comparison, Process::open, SL2.11a's verified connection and HostConnection::attach. Duplicate the process's existing OS wait object rather than creating a thread or event abstraction; return None for polling-only hosts. Keep the existing read-counter implementations and documentation unchanged.
    • Done when:
      • verify accepts matching saved runtime and process values, refuses a different runtime identity with the kind RuntimeMismatch, and opens no endpoint.
      • reattach refuses a runtime mismatch or wrong endpoint server and returns a verified process with an attached connection on success.
      • exit_handle returns an independently owned duplicated pidfd or Windows process handle, or None on a polling-only host; its cases in host/process/tests.rs use the platform's private no-pidfd helper.
      • Process::bytes_read, own_bytes_read and their Linux and Windows behavior keep their existing tests.
    • Needs: SL2.11a, above.
    • Estimate: 250 core lines, 5 counted files. Past 500 core lines, stop and ask before continuing.

Tests. stop returns Stopped only after a confirmed exit, and fails with the new error kind StillRunning when it can't confirm one; an unreadable or malformed report is an Io error in Stopped::report. report::read returns Ok(None) for a missing report. Process::wait returns None when its deadline passes first, and an Exited proof once the process exits. Process::open returns Exited only when no process of that ID and start time runs on this boot, or the boot has changed, never from the ID alone. verify and reattach refuse a different runtime identity with the kind RuntimeMismatch, and verify connects to nothing. exit_handle gives a duplicated handle, or None on a host that only polls. connect_verified refuses an endpoint that another process serves. Transport::unread, Process::bytes_read and own_bytes_read keep their tests.

Done when: every PR above has merged, with dev green.

Activity

  1. esaurez commented on Nov 10, 2026

    @esaurez
    ContributorAuthor

    Workstream source/progress checkpoint - 2026-11-10 UTC

    Checked boxes below mean merged scope, not merely an implemented first iteration. Implementation branches are source checkpoints, not automatic qualification, Ready or merge approval. Some starting inputs are frozen/anonymous commits rather than standalone branches; their exact SHAs and known merged producer refs are recorded explicitly.

    The original technical scope remains authoritative, supplemented only by any portable technical clarification in this issue. No machine-specific execution policy is part of this checkpoint.

  2. esaurez commented on Nov 10, 2026

    @esaurez
    ContributorAuthor

    Completed all five ordered scopes: SL2.9a (#1412), SL2.10 (#1432), SL2.10b (#1438), SL2.11a (#1406), and SL2.11b (#1414); all are merged. The whole-issue scope is unchanged and there are no subissues.

    PR1406's exact merged head 81f58751f77787bc3fc9e6abfe4a3cfe12696be1 passed the post-merge feature-branch push CI run 42678132782 and Required status check 129373929510; both actionable review threads are resolved. PR1438's exact head b5f66d7b60f37444cfb6a585e9b10a6cf012f8d1 passed CI run 42847634336 and Required status check 129946529426. Current dev head cbc20e07a2ec4ec90520d2490bda6ad27b8f3a52 has successful exact-head CI run 42862617590 and Required status check 130008525752.

    This does not relabel the old PR1406 COMMENTED/closer-look summary as a green review or formal APPROVED review, nor erase historical failed/cancelled runs or separate NFV failures. The observed dev aggregate was SUCCESS; successful required full CI is distinguished from any additional checks, not a blanket all-checks-green claim.

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions