Skip to content

packaging: add Arch forkd-bin recipe - #276

Merged
WaylandYang merged 3 commits into
deeplethe:mainfrom
Retengart:feature/arch-forkd-bin
Jul 31, 2026
Merged

packaging: add Arch forkd-bin recipe#276
WaylandYang merged 3 commits into
deeplethe:mainfrom
Retengart:feature/arch-forkd-bin

Conversation

@Retengart

@Retengart Retengart commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an Arch forkd-bin recipe for the official v0.5.3 release
  • depend on the distribution firecracker package instead of bundling one
  • install a hardened systemd unit, logrotate policy, license, and binaries
  • keep AUR publication separate from this upstream source recipe

The recipe patches the tag-pinned v0.5.3 unit only. The in-repo systemd unit is unchanged.

The service patch gives the controller access to its rootfs cache and permits the three syscalls observed for ip netns exec: mount, umount2, and capset. The existing @system-service allowlist remains; the explicit deny list mirrors @privileged except for required capset, avoiding exposure of the full @mount group.

Validation

  • makepkg --cleanbuild --force --noconfirm
  • source and release checksums verified by makepkg
  • .SRCINFO regenerated from PKGBUILD
  • bash -n packaging/arch/PKGBUILD
  • systemd-analyze verify /usr/lib/systemd/system/forkd-controller.service
  • pacman -Qkk forkd-bin firecracker: 0 altered files
  • installed package: forkd-bin 0.5.3-1
  • distribution Firecracker: 1.16.1-1.1
  • controller active using /usr/bin/forkd-controller
  • end-to-end controller test: create snapshot, spawn source, Diff BRANCH, spawn two per-child-netns children, execute divergent commands, cleanup
    • BRANCH pause: 9 ms
    • both children inherited the source /tmp state and exited 0
  • Gitleaks: one commit scanned, no leaks
  • no docs/superpowers/** paths

Scope

This PR now contains packaging files only. The MCP compatibility fix requested in #275 is split into a separate focused PR.

@Retengart
Retengart force-pushed the feature/arch-forkd-bin branch from ab442c3 to 6fe9c57 Compare July 31, 2026 11:21
@WaylandYang

Copy link
Copy Markdown
Contributor

Pre-review notes while this is still a draft — no formal review yet. Thanks for the unusually complete validation section; it made checking this much faster.

Verified independently

  • All four sha256sums match: LICENSE and packaging/systemd/forkd-controller.service recomputed from the v0.5.3 tag, the tarball against the release asset digest (417865e0…), and forkd.logrotate from the branch. .SRCINFO agrees with the PKGBUILD.
  • Sources are all first-party and tag-pinned. No third-party downloads, no curl | sh, no install= hook. package() installs exactly the five files the description lists, and creates no credentials.
  • forkd-v0.5.3-x86_64-linux.tar.gz unpacks to bare forkd / forkd-controller with no directory prefix, so the $srcdir/forkd assumptions in check() and package() hold.
  • --snapshot-root is a real flag (crates/forkd-controller/src/main.rs:41), so the prepare() insertion is valid.
  • CacheDirectory=forkd + /var/cache/forkd in ReadWritePaths is justified — that is the hub rootfs cache the controller reopens on restore (crates/forkd-cli/src/hub.rs:405).
  • copytruncate is the right call: the controller only installs a SIGTERM handler and AuditSink::open holds an append(true) handle for the process lifetime, so there is no reopen path to drive from postrotate today.

Please split this into two PRs

The mcp>=1.0mcp>=1.2,<2 fix is two lines plus a test, and #275 is a live user-facing breakage — a fresh pip install forkd-mcp today resolves MCP 2.x and produces a server that dies during initialize. The Arch recipe is ~200 lines that deserves an unhurried review.

Bundling them means users wait for the packaging review to get the fix. Suggest landing the sdk/mcp change plus test_stdio.py plus the mcp-python CI job on its own (that PR closes #275), and keeping this one for packaging/.

The systemd syscall-filter change needs its own justification

-SystemCallFilter=@system-service
-SystemCallFilter=~@privileged @resources
+SystemCallFilter=@system-service @mount
+SystemCallFilter=~@resources

The new comment explains why @mount is added, but not why the @privileged denial is dropped. I understand they are coupled in practice — a ~@privileged denylist re-denies the mount syscalls that @mount allows, so adding @mount alone would be inert. That coupling is worth stating in the comment, because the effect reaches well past mount: setns, capset, chroot, pivot_root, and bpf all become reachable. If a narrower deny set (enumerating the @privileged members forkd genuinely never needs, rather than dropping the group) was considered and rejected, saying so would settle it.

Two things make this worth pausing on:

  1. The PR summary files this under "install and harden the controller systemd unit". For the in-repo unit the syscall change is a net loosening, and it reads as hardening.
  2. The in-repo change is not required by the packaging work. prepare() patches the unit fetched from the v0.5.3 tag, not the working tree, so packaging/arch/ gets its patched copy either way. That makes packaging/systemd/forkd-controller.service an independent decision that happens to be riding along.

Related: this file's own convention is to document each relaxation inline — see the RestrictNamespaces block, which annotates every entry "so a well-intentioned trim doesn't silently break a feature". The new CacheDirectory and the @privileged removal are the two changes that don't follow it, and they are the two a future reader is most likely to question.

Release ordering

sdk/mcp/README.md and packaging/arch/README.md both tell users to run uv tool install 'forkd-mcp>=0.2.1,<0.3', while the PR states no PyPI publication happens here. Between merge and the actual 0.2.1 release that command fails with no matching distribution. Either land the docs with the release, or keep the pin unversioned until 0.2.1 is up.

Smaller things

  • pkgrel=3 — a new recipe conventionally starts at 1.
  • No CHANGELOG entry. ## Unreleased is currently empty, and a version bump with a user-visible dependency fix seems to qualify.
  • The mcp<2 matrix leg is unpinned, so a future 1.x release can turn CI red on an unrelated commit. fail-fast: false softens it, and this may well be the intended early-warning signal — worth a comment in the workflow saying so.
  • test_stdio.py imports anyio directly but nothing declares it; it arrives transitively through mcp. Adding it to the --with list would make the job independent of MCP's dependency graph.
  • Switching the documented install from pip install to uv tool install is an unrelated docs change. Also uv tool install --editable . is not really the from-source development path pip install -e . was — it installs a tool rather than putting the package in the current environment.

Follow-up worth filing separately

copytruncate loses any audit lines written between the copy and the truncate. That is a small window, but this is the audit log specifically, and silent gaps there are the kind of thing you want on record rather than discovered later. A SIGHUP handler that reopens AuditSink would let the policy switch to postrotate + systemctl reload. Not this PR's job — happy to open the issue if it is useful.

@Retengart
Retengart force-pushed the feature/arch-forkd-bin branch from 6fe9c57 to 5b3cbc3 Compare July 31, 2026 12:41
@Retengart Retengart changed the title packaging: add Arch forkd-bin and fix MCP compatibility packaging: add Arch forkd-bin recipe Jul 31, 2026
@Retengart
Retengart marked this pull request as ready for review July 31, 2026 12:43
@Retengart

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed pre-review. I split and rebased the work directly on main:

I also narrowed the generated package unit after testing the actual failure under systemd. ip netns exec needs mount, umount2, and capset; the recipe now adds only those to @system-service, retains ~@resources, and explicitly denies the remaining @privileged members. The unit comments and check() encode that boundary. It no longer exposes the full @mount group or broadly drops the privileged denial.

Revalidation covered a clean makepkg, package integrity checks, systemd-analyze verify, controller startup, and an actual controller API flow: base snapshot, source sandbox, diff branch, two per-child-netns children inheriting state, divergent writes, and complete cleanup.

The SIGHUP/audit-log follow-up is useful, but I kept it out of this packaging PR as suggested.

@WaylandYang

Copy link
Copy Markdown
Contributor

The split and the syscall narrowing both look right. I checked the narrowing against systemd's own table rather than reasoning about it, since that was the part I asked you to justify.

systemd-analyze syscall-filter @privileged reports:

@chown @clock @module @raw-io @reboot @swap
_sysctl acct bpf capset chroot fanotify_init fanotify_mark nfsservctl
open_by_handle_at pivot_root quotactl quotactl_fd setdomainname setfsuid
setfsuid32 setgroups setgroups32 sethostname setresuid setresuid32 setreuid
setreuid32 setuid setuid32 vhangup

The union of your two deny lines is exactly that set minus capset, plus @resources. So relative to the original ~@privileged @resources, the only syscalls that become reachable are mount, umount2, and capset — nothing else moved. That is what I was hoping for over dropping the group, and I had not realised @privileged nests six groups rather than just @chown; your first deny line catches all six.

Also re-verified after the rebase:

  • All four sha256sums still recompute correctly, and .SRCINFO agrees with the PKGBUILD including pkgrel = 1.
  • Both prepare() anchors still match the v0.5.3 unit exactly (lines 54 and 55), so the sed is not silently a no-op.
  • packaging/arch/** is the whole diff — the in-repo unit is untouched, as you said.
  • The pre-release forkd-mcp install line is gone from the Arch README.

Encoding the boundary in check() — asserting both the new lines and the absence of ~@privileged and the broad @mount — is a nice touch. It means a future well-intentioned edit that reintroduces either one fails the build rather than quietly widening the filter.

One optional nit, take it or leave it: a new distro package is arguably user-visible enough for a line under ## Unreleased, alongside the MCP entry in #277. Not a blocker.

Nothing else outstanding from my side. #277 is approved and going in first; this one is fine to land whenever.

@WaylandYang
WaylandYang merged commit c659539 into deeplethe:main Jul 31, 2026
5 checks passed
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.

2 participants