Skip to content

feat(det-init): guest pid-1 that detonates untrusted packages under a BPF-LSM observer - #983

Closed
bryan-minimal wants to merge 1 commit into
mainfrom
det/det-init-v2
Closed

feat(det-init): guest pid-1 that detonates untrusted packages under a BPF-LSM observer#983
bryan-minimal wants to merge 1 commit into
mainfrom
det/det-init-v2

Conversation

@bryan-minimal

@bryan-minimal bryan-minimal commented Jul 27, 2026

Copy link
Copy Markdown
Member

Adds det-init, the minimal-detonation guest /init. It embeds minimald::guest rather than forking minimald, then stands up an in-guest BPF-LSM observer and runs an untrusted package install as nobody under it, streaming the event feed out for a host-side verdict.

It deliberately does not run minimald's SSH server — a detonation is one-shot, so the sample runs directly and the guest is discarded.

Pure addition: one new crate (+664 lines), plus its Cargo.toml/Cargo.lock workspace entries. No existing file is modified.

Three load-bearing trust properties, each self-checked at boot

Property How it's proven
nobody cannot write the feed sample stdio is nulled, not inherited — otherwise it could print forged {"event":…} lines onto the console the host scrapes
the binding nonce is root-only leak-check asserts nobody gets EPERM
the event file is root-0400 uid-boundary self-check

The sample is bounded

The feed is drained after the sample returns, so an unbounded sample that never returns takes the entire feed with it — the host reaps the VM, the drain never runs, and a detonation that should have been BLOCK reports as an empty capture.

Not an exotic input: a poisoned package whose C2 is unreachable blocks on connect() exactly like this, so the better the network defense, the more likely the whole feed is lost. On expiry the sample is SIGKILLed and the events it already produced drain normally.

Verification

  • cargo check -p det-init --locked on Linux, rustc 1.97.0 (the repo's pinned toolchain) — clean
  • Live detonation on x86_64/KVM: 16/16 LSM hooks + 3/3 tracepoints attach; clean npm installPASS; poisoned sample (honeytoken reads + non-registry beacon) → BLOCK, signed

Supersedes #981, which carried six commits — including fixes to its own earlier commits and two headers over commitlint's 100-char limit. Squashed to one, since this introduces a single new crate and that history was noise to review.

Refs gominimal/minimal-detonation#194, gominimal/minimal-detonation#272

Note

Add det-init guest pid-1 to detonate untrusted packages under a BPF-LSM observer

  • Introduces a new det-init binary crate (Cargo.toml, main.rs) that runs as pid-1 inside the microVM and orchestrates the full detonation flow.
  • Mounts /dev, rootfs, securityfs, and bpffs; raises the memlock rlimit; then starts a root BPF-LSM observer (detonation-monitor) and waits up to 10s for it to signal readiness.
  • Executes the untrusted sample as uid 65534 (nobody) via a droppriv helper with a 90s deadline and SIGKILL backstop; emits the drained JSONL event feed to stdout for host collection.
  • Verifies security boundaries before sample launch: checks that nobody cannot read the binding nonce or write the event feed, and optionally repoints DNS to a local collector when egress is up.
  • Risk: as pid-1, any unhandled exit would cause a guest kernel panic; the process parks in an infinite sleep loop after async_main completes to prevent this.

Macroscope summarized fb15b54.

… BPF-LSM observer

Adds `det-init`, the minimal-detonation guest `/init`. It embeds minimald's public
guest-boot library (`minimald::guest`) rather than forking minimald, then stands up
an in-guest BPF-LSM observer and runs an untrusted package install as `nobody`
under it, streaming the resulting event feed out for a host-side verdict.

It deliberately does NOT run minimald's SSH server: a detonation is one-shot, so
the sample is executed directly and the guest is discarded afterwards.

Boot sequence: mount, enter the rootfs, emit the READY vsock beacon early (minvmd
tears the guest down on a ~5s READY timeout), start the observer and wait for it to
signal readiness so no syscall is missed, route the sample's DNS through the
observer's collector, fire the deny-channel tripwire, drop privileges, run the
sample, then drain the observer and emit the feed.

Three properties are load-bearing for the trust model and each is self-checked at
boot:

  - the sample's stdio is NULLED, not inherited. The feed is emitted on the guest
    console and the host collects it by scanning for `{`-lines, so an inherited
    console would let `nobody` print forged events that the host accepts as real —
    voiding "a nobody sample cannot write the feed".
  - the binding nonce is readable only by root, proven by a leak-check that asserts
    `nobody` gets EPERM.
  - the event file is root-0400, proven by a uid-boundary self-check.

The sample is bounded by its own deadline, shorter than the host's VM timeout. The
feed is drained AFTER the sample returns, so an unbounded sample that never returns
would take the entire feed with it — the host reaps the VM, the drain never runs,
and a detonation that should have been BLOCK reports as an empty capture. That is
not an exotic input: a poisoned package whose C2 is unreachable blocks on connect()
exactly like this, so the better the network defense, the more likely the whole
feed is lost. On expiry the sample is SIGKILLed and the events it already produced
are drained normally.

Verified on Linux (`cargo check -p det-init --locked`, rustc 1.97.0) and by live
detonation on x86_64/KVM: 16/16 LSM hooks and 3/3 tracepoints attach, a clean npm
install verdicts PASS, and a poisoned sample reading honeytokens and beaconing to a
non-registry host verdicts BLOCK.

Refs gominimal/minimal-detonation#194, gominimal/minimal-detonation#272
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5286f9e8-9086-4161-8443-c02194863a82

📥 Commits

Reviewing files that changed from the base of the PR and between b1cde76 and fb15b54.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • Cargo.toml
  • crates/det-init/Cargo.toml
  • crates/det-init/src/main.rs

Comment @coderabbitai help to get the list of available commands.

@bryan-minimal

Copy link
Copy Markdown
Member Author

Closing — this should not have been opened.

Landing det-init on minimal's main is gated on a minimal-team sync (observer position + untrusted-package policy), per the re-architecture decision tracked in gominimal/minimal-detonation#194. det-init deliberately lives on det/det-init-prototype, and the detonation substrate stages from that branch rather than from minimal's main — so nothing was blocked on merging it.

I opened this (and #981) without checking that gate. The CI churn on those PRs was the visible problem; the real one was proposing an architectural change to a shared repo that hasn't had its design conversation yet.

The det-init work itself is unaffected and stays on the branch: the hang fix (gominimal/minimal-detonation#272), the removed minimald version pin, and the missing Cargo.lock entry are all on det/det-init-v2. When the sync happens, that branch is what to propose from.

@bryan-minimal
bryan-minimal deleted the det/det-init-v2 branch July 30, 2026 00:31
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.

1 participant