Repository navigation
sandbox: identify OpenVMM by its process ID and start time - #378
Merged
Pedro Henrique Penna (ppenna) merged 1 commit intoOct 5, 2026
Merged
Conversation
`nvx.py sandbox` recorded only OpenVMM's process ID in runtime.json and counted any process with that ID as OpenVMM. On Linux, os.kill(pid, 0) succeeds for a zombie, and the PermissionError that another user's process raises also counted as running. On Windows, any process that the caller could open and that had not exited counted. So once OpenVMM was gone but its ID named a live process, the sandbox wedged: stop waited out --timeout on the dead control endpoint, deprovision refused because the sandbox seemed to run, and start refused the stale record. The ID names another process after OpenVMM crashes or the host restarts with a persistent --state-dir. And when no process reaps OpenVMM, as under a container init without --init, it stays a zombie, so even an acknowledged STOP ended in "OpenVMM did not terminate after managed stop" with runtime.json left behind. start now records OpenVMM's start time next to its ID, as the aci_edge_sandboxes backend does: the starttime field of /proc/<pid>/stat on Linux and the creation time from GetProcessTimes on Windows. It reads the time before it releases the child, so the ID cannot have been reused yet, and it fails without publishing a record if OpenVMM already exited or cannot be identified. A process counts as OpenVMM only while its start time matches the record, and a zombie counts as exited. On Windows, the check now waits on the process handle instead of comparing the exit code with STILL_ACTIVE, which an exit status of 259 also matches. A record that an earlier NVX version wrote has no start time and is still identified by its ID alone. When the process state cannot be read, the operation fails instead of guessing, so deprovision never removes the state of a VM that may still run. New unit tests cover the stat parser, a start-time mismatch, an exited and a zombie child, stop after an acknowledged STOP while OpenVMM stays a zombie, a reused ID through stop, exec, and deprovision, legacy and malformed records, and start failures. On bare-metal Linux/KVM, Linux/MSHV, and Windows/WHP hosts with real OpenVMM guests, the code before this change wedged in every case of the issue: an unreaped OpenVMM under a subreaper that never reaps, a record that names root-owned PID 1, a Linux PID that wraparound gave to an unrelated process, and a Windows PID recycled to cmd.exe. With this change, stop completes for the unreaped OpenVMM, and in the other cases exec and stop report stale state at once and deprovision succeeds. A sandbox that the previous code started still stops cleanly, and the managed-exec-config scenario passes on all three backends. Fixes #369 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The implementation matches the stated lifecycle requirements and includes comprehensive cross-platform coverage.
Review effort: Balanced
Findings: None
What changed in this PR
Adds robust OpenVMM process identity tracking to prevent stale or recycled PIDs from wedging managed sandbox operations.
Changes:
- Records and validates process start time across Linux and Windows.
- Treats exited and zombie processes as stopped while preserving legacy-record compatibility.
- Adds lifecycle tests and user documentation.
| File | Description |
|---|---|
scripts/nvx_tools/sandbox_lifecycle.py |
Implements PID/start-time identity checks. |
scripts/test_nvx_tools.py |
Covers reuse, zombies, malformed records, and startup failures. |
doc/run.md |
Documents process identity and legacy behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Pedro Henrique Penna (ppenna)
deleted the
fix/issue-369-sandbox-process-identity
branch
October 5, 2026 06:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
nvx.py sandboxnow identifies OpenVMM by its process ID and start time, so a recycled PID or an unreaped OpenVMM no longer wedgesstop,deprovision, andexec.Fixes #369
Problem
sandbox startrecorded only OpenVMM's PID inruntime.json, and_process_runningcounted any process with that PID as OpenVMM:os.kill(pid, 0)succeeds for a zombie, and thePermissionErrorthat another user's process raises also counted as running.Once OpenVMM was gone but its PID named a live process, no CLI operation recovered the sandbox:
stopwaited out--timeouton the dead control endpoint,deprovisionrefused ("sandbox must be stopped before deprovision"), andstartrefused the stale record. That happens after OpenVMM crashes or the host restarts with a persistent--state-dir. Separately, when nothing reaps OpenVMM (for example, a container whose PID 1 lacks--init), it stays a zombie, so even an acknowledged STOP ended in "OpenVMM did not terminate after managed stop" withruntime.jsonleft behind.Fix
The implementation follows the
ProcessIdentitydesign of theaci_edge_sandboxesbackend:startwritesstart_timenext topid. On Linux, it is field 22 (starttime) of/proc/<pid>/stat; on Windows, the creation time fromGetProcessTimes.startreads the time before it releases the child, so the PID cannot have been reused yet. If OpenVMM already exited or cannot be identified,startfails without publishing a record instead of waiting for the endpoint timeout.Z/X) counts as exited. On Windows, the check now waits on the process handle (WaitForSingleObject) instead of comparing the exit code withSTILL_ACTIVE, which an exit status of 259 also matches. A process that the caller cannot open still counts as gone, because OpenVMM runs as the caller.runtime.jsonwritten by an earlier NVX version has nostart_timeand is still identified by its PID alone, as before, except that a zombie now counts as exited./procorOpenProcesserror, or a malformed stat line), the operation fails with "cannot determine whether OpenVMM process N is running" instead of guessing.deprovisiontherefore never removes the state of a VM that may still run. A non-integer, boolean, or negativestart_timeis rejected.doc/run.mddescribes the identity check and how older records are treated.Tests
New tests in
scripts/test_nvx_tools.pycover:waitid(WNOWAIT))stopafter an acknowledged STOP while OpenVMM stays a zombie (Linux)stop,exec, anddeprovisionstartfailures when OpenVMM exits early or cannot be identifiedThe two existing
starttests now also check the recorded identity.Validation
I ran the real
scripts/nvx.py sandboxCLI against real OpenVMM microVMs on bare-metal Linux/KVM, Linux/MSHV, and Windows/WHP hosts. The artifacts were thev0.1.0-dev.2472412feb45release (OpenVMM974f356), ata3d929awith and without this change.stopfails with "OpenVMM did not terminate" after--timeout;startanddeprovisionrefuse;runtime.jsonstaysstopsucceeds in 0.2–0.4 s and removesruntime.jsonexecandstoptime out on the dead endpoint;startanddeprovisionrefuseexecandstopreport stale state in ≤0.4 s;deprovisionsucceedssleepreceived OpenVMM's PID (KVM, MSHV)cmd.exereceived OpenVMM's PID after 1.9k–23.7k process creations (WHP)provision,start,exec,stop,deprovision(all three)runtime.jsonincludesstart_timetest-microvm --scenario managed-exec-configOther checks:
validate-nvxunit suites (678 tests) pass on Windows, on the KVM host (Python 3.14), and on the MSHV host (Python 3.12).ruff check,ruff format --check, and strictpyrightfor both Linux and Windows are clean.ae0c3e9, which only updates thedata/baselines on top ofa3d929a. I re-ran the unit suites and quality gates there.sandbox starttime went from 1.943 s to 1.946 s. The identity check costs about 26–60 µs per call.Notes
startstill refuses any existingruntime.json, stale or not, which matches the expected behavior in the issue. To recover, rundeprovision(which now succeeds) andprovisionagain. Havingstartorstopclear stale records automatically would be a separate behavior change._process_running.