Skip to content

fix(microvm): reap the manager's whole process group on teardown - #207

Merged
dshakes merged 5 commits into
masterfrom
fix/itest-manager-cleanup
Jul 31, 2026
Merged

dshakes merged 5 commits into
masterfrom
fix/itest-manager-cleanup

Conversation

@dshakes

@dshakes dshakes commented Jul 31, 2026

Copy link
Copy Markdown
Owner

What

A failed integration-test.sh run left a live runtime-manager holding :50054. Every later run then failed to bind and reported "Firecracker backend did not report available" — a symptom pointing at the backend, or at whatever was last edited, rather than at the leaked process.

It cost three runs during the harness startup work and briefly looked like a code regression. The A/B that cleared it only worked because the host was cleaned by hand in between.

Why the existing kill wasn't enough

kill "${MANAGER_PID}" was already in cleanup. Two things compound past it:

  1. The manager does not reliably exit on SIGTERM.
  2. Its firecracker children are reparented rather than killed when it does.

Change

  • Manager starts under setsid; cleanup signals the whole process group — TERM, bounded wait, then KILL.
  • Not a host-wide pkill -f firecracker: that would kill unrelated VMs, and on the usual dev host (a Lima VM named firecracker-dev) the pattern also matches the caller's own shell command line and kills the session running the script.
  • Group id is read back via ps -o pgid= rather than assumed equal to the captured pid — setsid execs in place when the caller isn't already a group leader but forks when it is, and only one keeps that pid.
  • Teardown waits on the port, not the pid, and warns if it's still held — so a leak is reported by the run that caused it.
  • Pre-flight fails fast when the port is already taken, naming the cause and the command to find it.

Verification (real KVM, all three paths)

path result
forced failure (corrupt rootfs, panics after manager is up) manager 0, firecracker 0, port free — previously: manager alive, port held
normal run 5/5 assertions, host clean; TAP orphaned by the failed run went 1 → 0
port pre-occupied fails immediately with the real cause, not a backend error

The TAP result also confirms the manager's startup reconciliation covers the one thing a group-kill cannot.

A failed integration-test run left a live runtime-manager holding :50054. Every
LATER run then could not bind and reported "Firecracker backend did not report
available" — a symptom that points at the backend, or at whatever was last
edited, rather than at the leaked process. It cost three runs during the harness
startup work and briefly looked like a code regression; the A/B that cleared it
only worked because the host was cleaned by hand in between.

`kill "${MANAGER_PID}"` was already there. It was not enough, in two ways that
compound: the manager does not reliably exit on SIGTERM, and its firecracker
children are reparented rather than killed when it does.

So the manager now starts via `setsid`, and cleanup signals the whole process
group — TERM, a bounded wait for a graceful exit, then KILL. Deliberately not a
host-wide `pkill -f firecracker`: that would kill unrelated VMs, and on the
usual dev host (a Lima VM named `firecracker-dev`) the pattern also matches the
caller's own shell command line and kills the session running the script.

The group id is read back with `ps -o pgid=` rather than assumed equal to the
captured pid: `setsid` execs in place when the caller is not already a group
leader but forks when it is, and only one of those keeps that pid.

Teardown then waits for the PORT, not the pid — the port is what the next run
needs back — and warns if it is still held, so the leak is reported by the run
that caused it. A matching pre-flight fails fast when the port is already taken,
naming the likely cause and the command to find it, so the misleading message
above cannot recur.

Verified on real KVM, all three paths:
  - forced failure (corrupt rootfs, panics after the manager is up)
      -> manager 0, firecracker 0, port free   [previously: manager alive, port held]
  - normal run -> 5/5 assertions, host clean afterwards; the TAP orphaned by the
      failed run above went 1 -> 0, confirming the manager's startup
      reconciliation handles the one thing group-kill cannot
  - port pre-occupied -> fails immediately with the real cause, not a backend error
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Codex cross-audit (agent:audit)

No Blocking findings.

I audited only git diff origin/master...HEAD. The PR changes are limited to infra/firecracker/integration-test.sh, mainly adding a port_open preflight, process-group startup via setsid, and stronger cleanup. I did not find correctness, security, or missed edge-case regressions in the changed behavior.

Verification performed:

  • git diff --check origin/master...HEAD
  • bash -n infra/firecracker/integration-test.sh
  • shellcheck infra/firecracker/integration-test.sh where available

I could not run the live Firecracker path in this read-only/no-KVM environment, so runtime validation still depends on the KVM CI job.

@compass-sdlc-bot compass-sdlc-bot Bot added the agent:reviewed-clean Reviewer found no Blocking issues this round label Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔷 Gemini cross-audit (agent:audit-gemini)

Independent Security & Correctness Audit of pr-diff.patch


🚨 Blocking Correctness & Robustness Regressions

1. Race Condition and Immediate Failures under Job Control (set -m)

  • Mechanism: The patch launches the manager using setsid env ... & and immediately resolves its process group with MANAGER_PGID="$(ps -o pgid= -p "${MANAGER_PID}")".
  • The Bug: When the test script or any of its wrappers are executed with job control active (e.g., interactive terminals, or environments utilizing set -m), setsid detects that it is already a process group leader and forks a grandchild process while the parent child process (MANAGER_PID) immediately exits.
  • Impact:
    1. Immediate abort: The healthcheck loop (kill -0 "${MANAGER_PID}") will fail on the first iteration because MANAGER_PID has exited, terminating the integration test prematurely with a misleading "manager exited early" failure, even though the manager is actually running fine as a detached grandchild.
    2. Orphan leak: Because MANAGER_PID exited, ps will find no process and return empty, falling back to setting MANAGER_PGID to the dead MANAGER_PID. In cleanup(), kill -TERM "-${MANAGER_PGID}" will attempt to signal an empty/dead process group, leaving the actual running manager and its Firecracker VMs running and leaked.

2. Process Leak on Early Script Aborts

  • Mechanism: The updated cleanup() function handles termination only if MANAGER_PGID is non-empty:
    if [ -n "${MANAGER_PGID:-}" ]; then
      kill -TERM "-${MANAGER_PGID}" ...
    fi
  • The Bug: If the script fails or is terminated (e.g., via SIGINT / Ctrl+C) before line 238 (such as during CA generation, or during the cargo build of runtime-manager which can take substantial time), MANAGER_PGID is still unset/empty.
  • Impact: The manager process MANAGER_PID is started but is leaked completely, as cleanup() no longer falls back to killing MANAGER_PID when MANAGER_PGID is empty.

⚠️ Portability & Edge Cases

3. ps -o pgid= Portability Issues

  • The Bug: Specifying -o pgid= is standard on procps-ng (typical Linux desktop/VMs), but is unsupported in minimalist BusyBox or alpine-based container environments.
  • Impact: In environments with standard BusyBox ps, the command fails or exits with an error, rendering MANAGER_PGID empty and reverting to MANAGER_PID.

4. Word-Splitting on Unquoted Alternative Variable Expansion

  • The Bug: The parameter expansion ${FC_BINARY_PATH:+FC_BINARY_PATH="${FC_BINARY_PATH}"} is unquoted.
  • Impact: If a developer's environment path contains whitespaces (e.g., /home/dev/my firecracker/bin), bash will perform word-splitting, resulting in a syntax error when running the env command.
  • Fix: Safely quote the alternative expansion:
    "${FC_BINARY_PATH:+FC_BINARY_PATH=${FC_BINARY_PATH}}"

💡 Recommendation for a Cleaner & More Portable Design

To bypass setsid complexities, ps incompatibilities, and race conditions entirely, you can cleanly kill the process tree by signaling the manager's direct children first with pkill -P during cleanup:

cleanup() {
  if [ -n "${MANAGER_PID:-}" ]; then
    # Signal Firecracker children first so they aren't reparented to init
    pkill -TERM -P "${MANAGER_PID}" 2>/dev/null || true
    kill -TERM "${MANAGER_PID}" 2>/dev/null || true

    # Bounded wait for graceful shutdown
    for _ in $(seq 1 30); do
      port_open || break
      sleep 0.2
    done

    # Force kill if still holding onto sockets/resources
    pkill -KILL -P "${MANAGER_PID}" 2>/dev/null || true
    kill -KILL "${MANAGER_PID}" 2>/dev/null || true
  fi

  rm -rf "${WORK}" 2>/dev/null || true
}

This is fully portable, does not require setsid or ps -o pgid=, has zero race conditions under job-control environments, and guarantees clean teardown of both the manager and its hypervisor children.

@github-actions github-actions Bot added the domain:infra Touches infra / CI / deploy / migrations label Jul 31, 2026
@dshakes dshakes added the agent:audit Auditor (Codex): independent cross-audit label Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Codex cross-audit (agent:audit)

Findings

  • Non-blocking correctness risk: infra/firecracker/integration-test.sh:216 / 236
    The new cleanup relies on capturing the manager’s process group after setsid env ... &, but the fallback is wrong when setsid forks. In that case $! can refer to the short-lived setsid wrapper, ps -p "$MANAGER_PID" may return nothing, and the fallback sets MANAGER_PGID=$MANAGER_PID, which is not the actual manager/firecracker process group. Cleanup then signals a non-existent or wrong group and can still leak the manager. This mostly affects hand-run/job-control cases, but the script explicitly claims to support running by hand. A more robust pattern is to start a small wrapper under setsid that writes its own PGID/PID to a file after exec setup, or use setsid --wait with a child supervisor that records the real child PGID.

No Blocking issues found in the PR diff. Syntax checks passed; the CI-style non-interactive setsid sleep & case maps $! to the new session leader as expected in this environment.

@github-actions

Copy link
Copy Markdown
Contributor

🔷 Gemini cross-audit (agent:audit-gemini)

🚨 BLOCKING REGRESSIONS & CORRECTNESS ISSUES

1. Signal Storm / Shell Killing Itself in Non-Interactive Environments (CI)

  • The Bug: In non-interactive environments (e.g., GitHub Actions, where job control set -m is disabled by default), the background job setsid env ... & is spawned within the shell script's own process group. Therefore, ps -o pgid= -p "${MANAGER_PID}" resolves to the PGID of the test script itself.
  • The Consequence: During cleanup(), calling kill -TERM "-${MANAGER_PGID}" signals the script's own process group, killing the test script and aborting the runner prematurely before completing normal cleanup or reporting failures properly.

2. Process Leaks in Interactive Environments (Local Dev Hosts)

  • The Bug: When run in a process group leader shell (typical for local terminal runs), POSIX requirements force setsid to fork a child to establish the new session, exiting the parent process instantly.
  • The Consequence: Because the parent setsid process exits immediately:
    1. ps -p "${MANAGER_PID}" will query a dead parent/zombie whose PGID does not match the child's new session group ID.
    2. The cleanup signal kill -TERM "-${MANAGER_PGID}" targets the parent's process group, completely missing the actual runtime-manager and its child firecracker VM processes, leaving them orphaned and running.

3. Broken Startup & "Manager Exited Early" False Failures

  • The Bug: Because the parent setsid process exits immediately after forking:
    1. The startup check kill -0 "${MANAGER_PID}" will fail immediately on the first iteration once the parent process is reaped, resulting in a false-positive test failure: fail "manager exited early...".
    2. If the parent remains a zombie and hasn't been reaped, kill -0 succeeds but is completely blind to whether the actual manager or its VM children crash.

💡 PROPOSED FIX

To safely isolate the manager and its children in their own process group, capture the PGID reliably, and prevent leaks across all environments, use a subshell to write the PGID to a PID file before exec-ing the manager:

# Start the manager in a new session and write its PGID (which is the child's PID) to a file
setsid sh -c 'echo "$$" > "${WORK}/manager.pid"; exec env \
  RUNTIME_BACKEND=firecracker \
  LANTERN_RUNTIME_BACKEND=firecracker \
  LISTEN_ADDR="${MANAGER_LISTEN_ADDR}" \
  ... \
  "${MANAGER_BIN}"' >"${MANAGER_LOG}" 2>&1 &
MANAGER_PID=$!

# Read the correct, active PGID and PID
for _ in $(seq 1 50); do
  [ -s "${WORK}/manager.pid" ] && break
  sleep 0.1
done
[ -s "${WORK}/manager.pid" ] || fail "manager did not start"
MANAGER_REAL_PID=$(cat "${WORK}/manager.pid")
MANAGER_PGID="${MANAGER_REAL_PID}"

Then update the startup check and cleanup to reference ${MANAGER_REAL_PID} and ${MANAGER_PGID}:

  • Startup: kill -0 "${MANAGER_REAL_PID}" (properly tracks the live manager).
  • Cleanup: kill -TERM "-${MANAGER_PGID}" (reliably signals only the isolated manager + firecracker group).

…nference

Both PR auditors flagged the first cut, and they were right. `setsid` behaves
two different ways depending on whether its caller is already a process group
leader, and the patch guessed at both:

  - Caller NOT a leader (a script under CI, job control off): setsid execs in
    place, so the leader keeps the pid `$!` returned — but only after setsid()
    has actually run. Reading the pgid immediately after `&` is a race, and
    losing it yields the SCRIPT'S OWN pgid. Cleanup would then have killed this
    script's process group, taking the runner step with it. That is the very
    self-kill the comment two lines above warns about for `pkill -f
    firecracker`, reached from the other direction. My verification passed only
    because the race happened to resolve favourably.
  - Caller IS a leader (sourced from an interactive shell): setsid forks, `$!`
    is the short-lived wrapper. The liveness check would have called it dead at
    once ("manager exited early"), and the `ps` fallback would have signalled a
    group the manager was never in — leaking exactly what this PR set out to fix.

So stop inferring it. The manager now launches behind a one-line wrapper that
publishes its own `$$` and then execs the manager. Inside that wrapper `$$` is
the session leader in BOTH cases, and the exec keeps the pid, so the published
value is the manager's pid and its process group id at once. The script waits
for that file — a handshake, with no race left to lose — and uses it for
liveness too.

Cleanup additionally refuses to signal a group equal to this script's own. The
handshake should make that unreachable; it is there because the cost of being
wrong is the whole run plus its diagnostics.

Verified on real KVM across both setsid paths and the failure path:
  - job control OFF, normal run      -> 5/5, host clean
  - job control ON  (fork path),
    normal run                        -> 5/5, host clean  [previously: aborted
                                        immediately with "manager exited early"]
  - job control ON, forced failure    -> correct FAIL; manager 0, firecracker 0,
                                        port free  [previously: wrong group
                                        signalled, manager leaked]
The refusal guard never fired in any run, which is the expected result.
@github-actions

Copy link
Copy Markdown
Contributor

🛡️ Vuln scan — ❌ vulnerable dependency found

cd services/control-plane && govulncheck ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 15
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
cd services/gateway && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (272 crate dependencies)
cd services/model-router && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (251 crate dependencies)
cd services/runtime-manager && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (485 crate dependencies)
Crate:     wasmtime
Version:   45.0.3
Title:     Stores can mix up type indices between engines
Date:      2026-07-31
ID:        RUSTSEC-2026-0222
URL:       https://rustsec.org/advisories/RUSTSEC-2026-0222
Severity:  3.8 (low)
Solution:  Upgrade to >=24.0.12, <25.0.0 OR >=36.0.13, <37.0.0 OR >=46.0.2, <47.0.0 OR >=47.0.3

Crate:     event-listener
Version:   5.4.1
Warning:   unsound
Title:     `event-listener` allows `!Send` tags to cross thread boundaries via `StackSlot`
Date:      2026-07-13
ID:        RUSTSEC-2026-0221
URL:       https://rustsec.org/advisories/RUSTSEC-2026-0221

error: 1 vulnerability found!
warning: 1 allowed warning found
make: *** [Makefile:270: audit] Error 1

@dshakes dshakes added agent:audit Auditor (Codex): independent cross-audit and removed agent:audit Auditor (Codex): independent cross-audit labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Codex cross-audit (agent:audit)

Blocking

  • infra/firecracker/integration-test.sh:132 The new cleanup still does not reap leaked Firecracker VMs on failure. It kills only the manager’s process group, but Firecracker children are launched with libc::setsid() in firecracker.rs:1187, so each VM leaves the manager PGID before exec. If the script fails after Spawn and before the explicit Stop, cleanup terminates the manager but not the VM process it started. The port may free, so the new post-cleanup port check can pass while the actual leaked VM remains. Cleanup needs to stop known VM_IDs through the manager before killing it, or track and kill the spawned VM processes/process groups explicitly.

Notes

  • Syntax check passed: bash -n infra/firecracker/integration-test.sh.
  • I did not run the live Firecracker integration path in this environment.

@github-actions

Copy link
Copy Markdown
Contributor

🔷 Gemini cross-audit (agent:audit-gemini)

Blocking Correctness Regressions

  1. Manager Process Group Leak on Handshake Timeout / Early Failure

    • Issue: In the original script, MANAGER_PID=$! was set immediately upon launching the background manager, ensuring cleanup would kill the process if the subsequent health-check failed or timed out. In the patched script, MANAGER_PGID and MANAGER_PID are only assigned after the handshake loop successfully finishes:
      MANAGER_PGID="$(tr -dc '0-9' <"${MANAGER_PGID_FILE}" 2>/dev/null)"
      [ -n "${MANAGER_PGID}" ] || fail "manager wrapper never published its process group id"
      MANAGER_PID="${MANAGER_PGID}"
    • Impact: If the handshake loop times out (e.g., due to CPU scheduling delays on a heavily loaded CI runner), the script fails and triggers cleanup. Because MANAGER_PGID is unbound/empty, cleanup skips signaling, leaving the manager process running in the background and permanently holding port 50054. All subsequent test runs will fail to bind to the port.
    • Fix: Capture the background job PID immediately (WRAPPER_PID=$!) and use it as a fallback in cleanup if MANAGER_PGID is empty. In non-interactive CI runs, setsid execs in place, so WRAPPER_PID is exactly the manager's process group ID:
      # In cleanup():
      local pgid="${MANAGER_PGID:-}"
      if [ -z "${pgid}" ] && [ -n "${WRAPPER_PID:-}" ]; then
        pgid="${WRAPPER_PID}"
      fi
  2. Unbound Variable Error under set -u

    • Issue: The script runs with set -u (exit immediately on unbound variables). MANAGER_PGID is never declared or initialized at the top of the script. If the script exits before line 271, cleanup runs and evaluates:
      if [ -n "${MANAGER_PGID:-}" ] && [ "${MANAGER_PGID}" = "${SELF_PGID:-}" ]; then
    • Impact: Although && is short-circuiting, relying on it to bypass set -u is fragile. In some shells or execution paths, referencing the uninitialized "${MANAGER_PGID}" triggers an unbound variable error, terminating the cleanup function prematurely and leaving other temporary files and directories uncleaned.
    • Fix: Initialize MANAGER_PGID="" at the top of the script alongside other global variables.

Enhancements & Robustness

  1. Missing Precondition Tool Check for setsid

    • Issue: The patched script introduces a dependency on setsid. If setsid is missing (e.g., in a minimal Alpine/Debian-slim container or custom chroot environment), the background command fails, the script hangs for 5 seconds during the handshake, and then exits with a confusing timeout message.
    • Fix: Add setsid to the list of required tools checked at the top of the script to ensure immediate fail-fast behavior:
      for tool in grpcurl openssl jq setsid; do
        command -v "$tool" >/dev/null 2>&1 || fail "missing required tool: $tool"
      done
  2. Brittle SELF_PGID Extraction on Non-Standard Hosts

    • Issue: ps -o pgid= -p $$ is not portable and fails on minimal environments using BusyBox ps, leaving SELF_PGID empty and rendering the critical self-killing safeguard useless.
    • Fix: On Linux, retrieve the PGID directly and portably from /proc/$$/stat (the 5th field, or the 3rd field after the last closing parenthesis) using pure bash:
      SELF_PGID=""
      if [ -f /proc/$$/stat ]; then
        stat_content="$(cat /proc/$$/stat 2>/dev/null)"
        read -r _ _ pgrp _ <<< "${stat_content##*)}" 2>/dev/null || pgrp=""
        SELF_PGID="${pgrp}"
      fi
      if [ -z "${SELF_PGID}" ]; then
        SELF_PGID="$(ps -o pgid= -p $$ 2>/dev/null | tr -dc '0-9')"
      fi
  3. Wrapper Shell Path Portability

    • Issue: setsid bash -c ... relies on finding bash in the user's PATH.
    • Fix: Use setsid "$BASH" -c ... to guarantee executing with the exact same shell interpreter that is running the parent script.

Second round of auditor findings on this PR, both correct.

1. Killing the manager's process group never reaped its VMs. The backend
   launches each firecracker with `pre_exec(libc::setsid())`
   (backends/firecracker.rs), so every VM leaves the manager's session before
   exec and survives a group kill. A run failing after Spawn but before the
   explicit Stop left a live VM behind — and because the manager itself DID die,
   the port check came back clean and the run reported a tidy teardown while the
   VM leaked. That is the same shape of false green this file keeps producing:
   the check passed because it was measuring the wrong thing.

   My own verification missed it for the same reason. The forced-failure case I
   tested panics the guest, and a panicking VM exits on its own, so firecracker
   was 0 for a reason that had nothing to do with the cleanup being correct.

   Cleanup now issues Stop through the manager FIRST, while it is still alive.
   That is also the only path that releases the VM's TAP, so it fixes the tap
   orphan the previous round left behind rather than deferring it to the next
   run's reconciliation. A uuid-scoped backstop (matching this run's vm_id in
   the api socket path) covers a VM that outlives the manager anyway —
   deliberately not `pkill -f firecracker`, which matches the usual dev host's
   Lima VM name and this script's own command line.

2. `MANAGER_PGID` was only assigned after the handshake, so a handshake that
   timed out while the manager was running leaked it — a regression against the
   old `MANAGER_PID=$!`, which covered exactly that window. The launch pid is
   now recorded before the handshake and used as the fallback, and cleanup
   re-reads the pgid file in case it was published late.

Verified on real KVM by fault injection — a synthetic failure after a HEALTHY
boot, which is the case that leaks (the panicking guest hid it):

  previous commit -> firecracker 1 LEAKED, tap 1, port free (false green)
  this commit     -> firecracker 0, tap 0, port free

Plus the three paths from the previous round still clean: job control on and
off, and the forced-failure case.
@dshakes dshakes added agent:audit Auditor (Codex): independent cross-audit and removed agent:audit Auditor (Codex): independent cross-audit labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 QA — ❌ tests failing

cd services/control-plane && go test -race -count=1 ./...
?   	github.com/dshakes/lantern/services/control-plane/cmd/migrate	[no test files]
?   	github.com/dshakes/lantern/services/control-plane/cmd/reencrypt-credentials	[no test files]
ok  	github.com/dshakes/lantern/services/control-plane/cmd/server	1.022s
ok  	github.com/dshakes/lantern/services/control-plane/internal/agentidentity	1.017s
ok  	github.com/dshakes/lantern/services/control-plane/internal/db	1.008s
ok  	github.com/dshakes/lantern/services/control-plane/internal/grounding	1.007s
ok  	github.com/dshakes/lantern/services/control-plane/internal/handlers	1.265s
ok  	github.com/dshakes/lantern/services/control-plane/internal/middleware	1.011s
ok  	github.com/dshakes/lantern/services/control-plane/internal/scheduler	1.021s
ok  	github.com/dshakes/lantern/services/control-plane/internal/secrets	1.006s
?   	github.com/dshakes/lantern/services/control-plane/internal/server	[no test files]
?   	github.com/dshakes/lantern/services/control-plane/internal/storage	[no test files]
ok  	github.com/dshakes/lantern/services/control-plane/internal/telemetry	1.010s
ok  	github.com/dshakes/lantern/services/control-plane/internal/workflow	1.016s
cd services/workflow-engine && go test -race -count=1 ./...
?   	github.com/dshakes/lantern/services/workflow-engine/cmd/server	[no test files]
?   	github.com/dshakes/lantern/services/workflow-engine/internal/db	[no test files]
ok  	github.com/dshakes/lantern/services/workflow-engine/internal/engine	1.027s
?   	github.com/dshakes/lantern/services/workflow-engine/internal/handlers	[no test files]
ok  	github.com/dshakes/lantern/services/workflow-engine/internal/journal	1.011s
ok  	github.com/dshakes/lantern/services/workflow-engine/internal/middleware	1.011s
?   	github.com/dshakes/lantern/services/workflow-engine/internal/server	[no test files]
cd services/scheduler && go test -race -count=1 ./...
?   	github.com/dshakes/lantern/services/scheduler/cmd/server	[no test files]
ok  	github.com/dshakes/lantern/services/scheduler/internal/cron	1.011s
?   	github.com/dshakes/lantern/services/scheduler/internal/db	[no test files]
ok  	github.com/dshakes/lantern/services/scheduler/internal/handlers	1.022s
?   	github.com/dshakes/lantern/services/scheduler/internal/middleware	[no test files]
ok  	github.com/dshakes/lantern/services/scheduler/internal/runclient	1.027s
?   	github.com/dshakes/lantern/services/scheduler/internal/server	[no test files]
cd services/runtime-scheduler && CGO_ENABLED=0 go test ./...
?   	github.com/dshakes/lantern/services/runtime-scheduler/cmd/scheduler	[no test files]
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/cluster	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/dialer	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/handlers	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/leader	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/metrics	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/middleware	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/placement	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/scoring	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/store	(cached)
cd services/gateway && cargo test
info: syncing channel updates for 1.93-x86_64-unknown-linux-gnu
info: latest update on 2026-02-12 for version 1.93.1 (01f6ddf75 2026-02-11)
info: recovering from a partially installed toolchain
info: removing previous version of component rustc
info: removing previous version of component cargo
info: removing previous version of component rust-std
info: removing previous version of component rust-docs
info: removing previous version of component rustfmt
info: removing previous version of component clippy
info: downloading 6 components
info: rolling back changes
error: failed to install component: 'clippy-preview-x86_64-unknown-linux-gnu', detected conflict: 'bin/cargo-clippy'
make: *** [Makefile:186: test-rust] Error 1

@github-actions

Copy link
Copy Markdown
Contributor

🔎 Codex cross-audit (agent:audit)

Blocking: none found.

Findings:

  • Non-blocking: infra/firecracker/integration-test.sh:184 only kills leaked Firecracker processes whose argv contains /run/firecracker/${VM_ID}.sock. That misses jailer mode, where the backend uses a host-visible socket under the jail root instead of the bare /run/firecracker/<vm>.sock path. If FIRECRACKER_JAILER_PATH is set and the manager dies before Stop succeeds, this new backstop can still leak the VM. Either make the match jailer-aware or explicitly unset/disallow jailer mode in this integration script.

Validation run:

  • bash -n infra/firecracker/integration-test.sh
  • shellcheck infra/firecracker/integration-test.sh only reported SC2016 on the intentional single-quoted bash -c wrapper.

@github-actions

Copy link
Copy Markdown
Contributor

🔷 Gemini cross-audit (agent:audit-gemini)

Audit Report: infra/firecracker/integration-test.sh

1. Blocking Issues / Correctness Regressions

  • None. The changes to session and process group management, along with the precise PID file handshake, are robust and cleanly solve the manager and VM process leakage issues.

2. Security Issues & Safeguards

  • Option-parsing Safeguard for Process Group Signaling:
    The script uses kill -TERM "-${MANAGER_PGID}". To prevent various shell built-in or utility kill implementations from interpreting the negative PGID as a flag, use the standard -- end-of-options delimiter:
    kill -TERM -- "-${MANAGER_PGID}"
    kill -KILL -- "-${MANAGER_PGID}"
  • Robustness of SELF_PGID Safeguard:
    If ps fails or is not present, SELF_PGID will be empty. The safety comparison [ "${MANAGER_PGID}" = "${SELF_PGID:-}" ] would then fail to protect the parent process group. Adding a check to ensure SELF_PGID is non-empty improves defense-in-depth.

3. Missed Edge Cases & Robustness Improvements

  • Hanging grpcurl in cleanup():
    In cleanup(), grpcurl is called to stop the VM:
    grpcurl -cacert "${WORK}/ca.crt" -cert "${WORK}/client.crt" -key "${WORK}/client.key" ... lantern.v1.RuntimeManager/Stop
    If the runtime manager is deadlocked or frozen, this call can hang indefinitely. Since cleanup() is registered as an exit trap, it will block the CI runner or shell session. Adding a timeout flag (e.g., -max-time 5) ensures that cleanup() cannot block indefinitely:
    grpcurl -max-time 5 -cacert ...
  • Leakage of ca.srl (CA Serial File):
    Running openssl x509 -req ... -CAcreateserial creates a file named ca.srl in the current working directory (the repository root), which is untracked by Git and left behind after the run because it is not within ${WORK}. To keep the workspace clean, specify the serial path explicitly using the -CAserial flag:
    openssl x509 -req -in "${WORK}/server.csr" -CA "${WORK}/ca.crt" -CAkey "${WORK}/ca.key" \
      -CAcreateserial -CAserial "${WORK}/ca.srl" -days 1 -out "${WORK}/server.crt"

@github-actions

Copy link
Copy Markdown
Contributor

🛡️ Vuln scan — ❌ vulnerable dependency found

cd services/control-plane && govulncheck ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 15
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
cd services/gateway && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (272 crate dependencies)
cd services/model-router && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (251 crate dependencies)
cd services/runtime-manager && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (485 crate dependencies)
Crate:     wasmtime
Version:   45.0.3
Title:     Stores can mix up type indices between engines
Date:      2026-07-31
ID:        RUSTSEC-2026-0222
URL:       https://rustsec.org/advisories/RUSTSEC-2026-0222
Severity:  3.8 (low)
Solution:  Upgrade to >=24.0.12, <25.0.0 OR >=36.0.13, <37.0.0 OR >=46.0.2, <47.0.0 OR >=47.0.3

Crate:     event-listener
Version:   5.4.1
Warning:   unsound
Title:     `event-listener` allows `!Send` tags to cross thread boundaries via `StackSlot`
Date:      2026-07-13
ID:        RUSTSEC-2026-0221
URL:       https://rustsec.org/advisories/RUSTSEC-2026-0221

error: 1 vulnerability found!
warning: 1 allowed warning found
make: *** [Makefile:270: audit] Error 1

Both non-blocking findings from the third audit round.

The backstop matched `/run/firecracker/<vm_id>.sock`, which is the socket path
only in the default configuration. Under jailer mode (FIRECRACKER_JAILER_PATH)
the socket lives inside the jail root instead, so the match would miss and the
VM would still leak — the exact case the backstop exists for. It now matches the
vm_id anywhere in the argv of a process actually named `firecracker`, which
covers both layouts. Scope is unchanged: the id is a uuid minted for this run,
and restricting to `pgrep -x firecracker` means it cannot match another run, an
unrelated hypervisor, or this script.

Group kills pass `--` so a negative pgid can never be parsed as an option by a
kill implementation that reads it that way.

Verified on real KVM: fault injection after a healthy boot still leaves manager
0, firecracker 0, taps 0, port free; and a normal run is 5/5 with a clean host.
@github-actions

Copy link
Copy Markdown
Contributor

🧪 QA — ❌ tests failing

cd services/control-plane && go test -race -count=1 ./...
?   	github.com/dshakes/lantern/services/control-plane/cmd/migrate	[no test files]
?   	github.com/dshakes/lantern/services/control-plane/cmd/reencrypt-credentials	[no test files]
ok  	github.com/dshakes/lantern/services/control-plane/cmd/server	1.028s
ok  	github.com/dshakes/lantern/services/control-plane/internal/agentidentity	1.017s
ok  	github.com/dshakes/lantern/services/control-plane/internal/db	1.008s
ok  	github.com/dshakes/lantern/services/control-plane/internal/grounding	1.007s
ok  	github.com/dshakes/lantern/services/control-plane/internal/handlers	1.270s
ok  	github.com/dshakes/lantern/services/control-plane/internal/middleware	1.011s
ok  	github.com/dshakes/lantern/services/control-plane/internal/scheduler	1.020s
ok  	github.com/dshakes/lantern/services/control-plane/internal/secrets	1.007s
?   	github.com/dshakes/lantern/services/control-plane/internal/server	[no test files]
?   	github.com/dshakes/lantern/services/control-plane/internal/storage	[no test files]
ok  	github.com/dshakes/lantern/services/control-plane/internal/telemetry	1.010s
ok  	github.com/dshakes/lantern/services/control-plane/internal/workflow	1.016s
cd services/workflow-engine && go test -race -count=1 ./...
?   	github.com/dshakes/lantern/services/workflow-engine/cmd/server	[no test files]
?   	github.com/dshakes/lantern/services/workflow-engine/internal/db	[no test files]
ok  	github.com/dshakes/lantern/services/workflow-engine/internal/engine	1.026s
?   	github.com/dshakes/lantern/services/workflow-engine/internal/handlers	[no test files]
ok  	github.com/dshakes/lantern/services/workflow-engine/internal/journal	1.011s
ok  	github.com/dshakes/lantern/services/workflow-engine/internal/middleware	1.012s
?   	github.com/dshakes/lantern/services/workflow-engine/internal/server	[no test files]
cd services/scheduler && go test -race -count=1 ./...
?   	github.com/dshakes/lantern/services/scheduler/cmd/server	[no test files]
ok  	github.com/dshakes/lantern/services/scheduler/internal/cron	1.013s
?   	github.com/dshakes/lantern/services/scheduler/internal/db	[no test files]
ok  	github.com/dshakes/lantern/services/scheduler/internal/handlers	1.024s
?   	github.com/dshakes/lantern/services/scheduler/internal/middleware	[no test files]
ok  	github.com/dshakes/lantern/services/scheduler/internal/runclient	1.027s
?   	github.com/dshakes/lantern/services/scheduler/internal/server	[no test files]
cd services/runtime-scheduler && CGO_ENABLED=0 go test ./...
?   	github.com/dshakes/lantern/services/runtime-scheduler/cmd/scheduler	[no test files]
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/cluster	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/dialer	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/handlers	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/leader	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/metrics	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/middleware	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/placement	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/scoring	(cached)
ok  	github.com/dshakes/lantern/services/runtime-scheduler/internal/store	(cached)
cd services/gateway && cargo test
info: syncing channel updates for 1.93-x86_64-unknown-linux-gnu
info: latest update on 2026-02-12 for version 1.93.1 (01f6ddf75 2026-02-11)
info: recovering from a partially installed toolchain
info: removing previous version of component rustc
info: removing previous version of component cargo
info: removing previous version of component rust-std
info: removing previous version of component rust-docs
info: removing previous version of component rustfmt
info: removing previous version of component clippy
info: downloading 6 components
info: rolling back changes
error: failed to install component: 'clippy-preview-x86_64-unknown-linux-gnu', detected conflict: 'bin/cargo-clippy'
make: *** [Makefile:186: test-rust] Error 1

@github-actions

Copy link
Copy Markdown
Contributor

🛡️ Vuln scan — ❌ vulnerable dependency found

cd services/control-plane && govulncheck ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 15
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
cd services/gateway && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (272 crate dependencies)
cd services/model-router && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (251 crate dependencies)
cd services/runtime-manager && cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 1177 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (485 crate dependencies)
Crate:     wasmtime
Version:   45.0.3
Title:     Stores can mix up type indices between engines
Date:      2026-07-31
ID:        RUSTSEC-2026-0222
URL:       https://rustsec.org/advisories/RUSTSEC-2026-0222
Severity:  3.8 (low)
Solution:  Upgrade to >=24.0.12, <25.0.0 OR >=36.0.13, <37.0.0 OR >=46.0.2, <47.0.0 OR >=47.0.3

Crate:     event-listener
Version:   5.4.1
Warning:   unsound
Title:     `event-listener` allows `!Send` tags to cross thread boundaries via `StackSlot`
Date:      2026-07-13
ID:        RUSTSEC-2026-0221
URL:       https://rustsec.org/advisories/RUSTSEC-2026-0221

error: 1 vulnerability found!
warning: 1 allowed warning found
make: *** [Makefile:270: audit] Error 1

@dshakes
dshakes merged commit 1b3c6a6 into master Jul 31, 2026
9 checks passed
@dshakes
dshakes deleted the fix/itest-manager-cleanup branch July 31, 2026 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:audit Auditor (Codex): independent cross-audit agent:reviewed-clean Reviewer found no Blocking issues this round domain:infra Touches infra / CI / deploy / migrations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant