feat(image): binfmt_misc in the guest kernel (#37)
- commit
- 76a5504f0cea61dde10c7d4fb9751907a31eaaed
- author
- brice · · 2026-10-16 14:12 UTC
- committer
- bnema · · 2026-10-16 14:12 UTC
- parent
- 65c7d3d
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown - refs
- tag: v0.4.0
4 files · +58 -0
M README.md +2 -0
| @@ -382,6 +382,8 @@ scripts/build-image.sh tmp/image # builds the OCI image with docker or podman | |||
| 382 | 382 | ||
| 383 | 383 | The kernel configuration is `images/kernel/config-6.18` (+ per-architecture fragments), checked after `olddefconfig`: the build fails if a listed option did not survive. arm64 builds (`ARCH_NAME=arm64`) but is not tested in CI-1. | |
| 384 | 384 | ||
| 385 | + | `CONFIG_BINFMT_MISC` is built in (the guest has no modules) and the guest init mounts it at `/proc/sys/fs/binfmt_misc`, as systemd does on GitHub's image, so `docker/setup-qemu-action` (`tonistiigi/binfmt --install`) can register QEMU handlers and run foreign-architecture containers. The handlers belong to the job's VM and disappear with it. With a kernel built before this option, the init logs the failed mount and jobs run without it. | |
| 386 | + | ||
| 385 | 387 | ## Tests | |
| 386 | 388 | ||
| 387 | 389 | ```sh | |
| @@ -382,6 +382,8 @@ scripts/build-image.sh tmp/image # builds the OCI image with docker or podman | |||||
| 382 | 382 | ||||
| 383 | The kernel configuration is `images/kernel/config-6.18` (+ per-architecture fragments), checked after `olddefconfig`: the build fails if a listed option did not survive. arm64 builds (`ARCH_NAME=arm64`) but is not tested in CI-1. | 383 | The kernel configuration is `images/kernel/config-6.18` (+ per-architecture fragments), checked after `olddefconfig`: the build fails if a listed option did not survive. arm64 builds (`ARCH_NAME=arm64`) but is not tested in CI-1. | ||
| 384 | 384 | ||||
| 385 | + | `CONFIG_BINFMT_MISC` is built in (the guest has no modules) and the guest init mounts it at `/proc/sys/fs/binfmt_misc`, as systemd does on GitHub's image, so `docker/setup-qemu-action` (`tonistiigi/binfmt --install`) can register QEMU handlers and run foreign-architecture containers. The handlers belong to the job's VM and disappear with it. With a kernel built before this option, the init logs the failed mount and jobs run without it. | |||
| 386 | + | ||||
| 385 | ## Tests | 387 | ## Tests | ||
| 386 | 388 | ||||
| 387 | ```sh | 389 | ```sh | ||
M images/kernel/config-6.18 +1 -0
| @@ -31,6 +31,7 @@ CONFIG_HIGH_RES_TIMERS=y | |||
| 31 | 31 | CONFIG_NO_HZ_IDLE=y | |
| 32 | 32 | CONFIG_BINFMT_ELF=y | |
| 33 | 33 | CONFIG_BINFMT_SCRIPT=y | |
| 34 | + | CONFIG_BINFMT_MISC=y | |
| 34 | 35 | CONFIG_SYSVIPC=y | |
| 35 | 36 | CONFIG_POSIX_MQUEUE=y | |
| 36 | 37 | CONFIG_KEYS=y | |
| @@ -31,6 +31,7 @@ CONFIG_HIGH_RES_TIMERS=y | |||||
| 31 | CONFIG_NO_HZ_IDLE=y | 31 | CONFIG_NO_HZ_IDLE=y | ||
| 32 | CONFIG_BINFMT_ELF=y | 32 | CONFIG_BINFMT_ELF=y | ||
| 33 | CONFIG_BINFMT_SCRIPT=y | 33 | CONFIG_BINFMT_SCRIPT=y | ||
| 34 | + | CONFIG_BINFMT_MISC=y | |||
| 34 | CONFIG_SYSVIPC=y | 35 | CONFIG_SYSVIPC=y | ||
| 35 | CONFIG_POSIX_MQUEUE=y | 36 | CONFIG_POSIX_MQUEUE=y | ||
| 36 | CONFIG_KEYS=y | 37 | CONFIG_KEYS=y | ||
M internal/guest/vmsetup.go +7 -0
| @@ -140,6 +140,13 @@ func setupVM(log zerowrap.Logger, args map[string]string) error { | |||
| 140 | 140 | log.Warn().Err(err).Str("target", m.dst).Msg("init: mount") | |
| 141 | 141 | } | |
| 142 | 142 | } | |
| 143 | + | // Mounted at boot on GitHub's image (systemd's | |
| 144 | + | // proc-sys-fs-binfmt_misc.automount): jobs read the handlers that | |
| 145 | + | // setup-qemu-action registers there. A kernel without BINFMT_MISC has no | |
| 146 | + | // such directory; the jobs run without it. | |
| 147 | + | if err := mount("binfmt_misc", "/proc/sys/fs/binfmt_misc", "binfmt_misc", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC, ""); err != nil { | |
| 148 | + | log.Warn().Err(err).Msg("init: mount binfmt_misc") | |
| 149 | + | } | |
| 143 | 150 | _ = os.Chmod("/tmp", sandbox.TmpMode) | |
| 144 | 151 | ||
| 145 | 152 | gw := args[sandbox.CmdlineGateway] | |
| @@ -140,6 +140,13 @@ func setupVM(log zerowrap.Logger, args map[string]string) error { | |||||
| 140 | log.Warn().Err(err).Str("target", m.dst).Msg("init: mount") | 140 | log.Warn().Err(err).Str("target", m.dst).Msg("init: mount") | ||
| 141 | } | 141 | } | ||
| 142 | } | 142 | } | ||
| 143 | + | // Mounted at boot on GitHub's image (systemd's | |||
| 144 | + | // proc-sys-fs-binfmt_misc.automount): jobs read the handlers that | |||
| 145 | + | // setup-qemu-action registers there. A kernel without BINFMT_MISC has no | |||
| 146 | + | // such directory; the jobs run without it. | |||
| 147 | + | if err := mount("binfmt_misc", "/proc/sys/fs/binfmt_misc", "binfmt_misc", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC, ""); err != nil { | |||
| 148 | + | log.Warn().Err(err).Msg("init: mount binfmt_misc") | |||
| 149 | + | } | |||
| 143 | _ = os.Chmod("/tmp", sandbox.TmpMode) | 150 | _ = os.Chmod("/tmp", sandbox.TmpMode) | ||
| 144 | 151 | ||||
| 145 | gw := args[sandbox.CmdlineGateway] | 152 | gw := args[sandbox.CmdlineGateway] | ||
M internal/sandbox/integration/vm_test.go +48 -0
| @@ -305,3 +305,51 @@ grep ' /home/runner/externals ' /proc/mounts | |||
| 305 | 305 | t.Error("the externals are writable from the guest") | |
| 306 | 306 | } | |
| 307 | 307 | } | |
| 308 | + | ||
| 309 | + | // TestVMBinfmtMisc checks what docker/setup-qemu-action needs from the guest: | |
| 310 | + | // binfmt_misc mounted at boot, as on GitHub's image, and a handler registered | |
| 311 | + | // with the F (fix binary) flag that tonistiigi/binfmt uses. The handler here is | |
| 312 | + | // cat on an extension, so the test needs neither QEMU nor the network. | |
| 313 | + | func TestVMBinfmtMisc(t *testing.T) { | |
| 314 | + | if os.Getenv("ERMITE_RUNNER_IT_VM") != "1" { | |
| 315 | + | t.Skip("set ERMITE_RUNNER_IT_VM=1") | |
| 316 | + | } | |
| 317 | + | need(t, "mkfs.ext4") | |
| 318 | + | needNetAdmin(t) | |
| 319 | + | ctx := ctxWithLog(t) | |
| 320 | + | cfg := loadConfig(t, sandbox.VM) | |
| 321 | + | drv, err := sandbox.Select(ctx, cfg, drivers.Factories(), sandbox.Prober{}) | |
| 322 | + | if err != nil { | |
| 323 | + | t.Fatal(err) | |
| 324 | + | } | |
| 325 | + | defer func() { _ = drv.Close() }() | |
| 326 | + | _ = drv.Reap(ctx) | |
| 327 | + | sb, err := drv.New() | |
| 328 | + | if err != nil { | |
| 329 | + | t.Fatal(err) | |
| 330 | + | } | |
| 331 | + | defer func() { _ = sb.Stop(context.Background()) }() | |
| 332 | + | conn, err := sb.Start(ctx, sandbox.JobSpec{JobID: "binfmt"}) | |
| 333 | + | if err != nil { | |
| 334 | + | t.Fatal(err) | |
| 335 | + | } | |
| 336 | + | r, err := runJob(ctx, conn, ` | |
| 337 | + | S=; [ "$(id -u)" = 0 ] || S="sudo -n" | |
| 338 | + | grep ' /proc/sys/fs/binfmt_misc ' /proc/mounts | |
| 339 | + | echo "status: $(cat /proc/sys/fs/binfmt_misc/status)" | |
| 340 | + | echo ':ermitetest:E::ermitetest::/usr/bin/cat:F' | $S tee /proc/sys/fs/binfmt_misc/register >/dev/null | |
| 341 | + | grep -x 'flags: F' /proc/sys/fs/binfmt_misc/ermitetest | |
| 342 | + | printf 'ran through binfmt_misc\n' > /tmp/probe.ermitetest | |
| 343 | + | chmod +x /tmp/probe.ermitetest | |
| 344 | + | /tmp/probe.ermitetest | |
| 345 | + | `) | |
| 346 | + | if err != nil { | |
| 347 | + | t.Fatal(err) | |
| 348 | + | } | |
| 349 | + | t.Log("\n" + r.out + r.errOut) | |
| 350 | + | for _, w := range []string{"binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc", "status: enabled\n", "flags: F\n", "ran through binfmt_misc\n"} { | |
| 351 | + | if !strings.Contains(r.out, w) { | |
| 352 | + | t.Errorf("output lacks %q", w) | |
| 353 | + | } | |
| 354 | + | } | |
| 355 | + | } | |
| @@ -305,3 +305,51 @@ grep ' /home/runner/externals ' /proc/mounts | |||||
| 305 | t.Error("the externals are writable from the guest") | 305 | t.Error("the externals are writable from the guest") | ||
| 306 | } | 306 | } | ||
| 307 | } | 307 | } | ||
| 308 | + | ||||
| 309 | + | // TestVMBinfmtMisc checks what docker/setup-qemu-action needs from the guest: | |||
| 310 | + | // binfmt_misc mounted at boot, as on GitHub's image, and a handler registered | |||
| 311 | + | // with the F (fix binary) flag that tonistiigi/binfmt uses. The handler here is | |||
| 312 | + | // cat on an extension, so the test needs neither QEMU nor the network. | |||
| 313 | + | func TestVMBinfmtMisc(t *testing.T) { | |||
| 314 | + | if os.Getenv("ERMITE_RUNNER_IT_VM") != "1" { | |||
| 315 | + | t.Skip("set ERMITE_RUNNER_IT_VM=1") | |||
| 316 | + | } | |||
| 317 | + | need(t, "mkfs.ext4") | |||
| 318 | + | needNetAdmin(t) | |||
| 319 | + | ctx := ctxWithLog(t) | |||
| 320 | + | cfg := loadConfig(t, sandbox.VM) | |||
| 321 | + | drv, err := sandbox.Select(ctx, cfg, drivers.Factories(), sandbox.Prober{}) | |||
| 322 | + | if err != nil { | |||
| 323 | + | t.Fatal(err) | |||
| 324 | + | } | |||
| 325 | + | defer func() { _ = drv.Close() }() | |||
| 326 | + | _ = drv.Reap(ctx) | |||
| 327 | + | sb, err := drv.New() | |||
| 328 | + | if err != nil { | |||
| 329 | + | t.Fatal(err) | |||
| 330 | + | } | |||
| 331 | + | defer func() { _ = sb.Stop(context.Background()) }() | |||
| 332 | + | conn, err := sb.Start(ctx, sandbox.JobSpec{JobID: "binfmt"}) | |||
| 333 | + | if err != nil { | |||
| 334 | + | t.Fatal(err) | |||
| 335 | + | } | |||
| 336 | + | r, err := runJob(ctx, conn, ` | |||
| 337 | + | S=; [ "$(id -u)" = 0 ] || S="sudo -n" | |||
| 338 | + | grep ' /proc/sys/fs/binfmt_misc ' /proc/mounts | |||
| 339 | + | echo "status: $(cat /proc/sys/fs/binfmt_misc/status)" | |||
| 340 | + | echo ':ermitetest:E::ermitetest::/usr/bin/cat:F' | $S tee /proc/sys/fs/binfmt_misc/register >/dev/null | |||
| 341 | + | grep -x 'flags: F' /proc/sys/fs/binfmt_misc/ermitetest | |||
| 342 | + | printf 'ran through binfmt_misc\n' > /tmp/probe.ermitetest | |||
| 343 | + | chmod +x /tmp/probe.ermitetest | |||
| 344 | + | /tmp/probe.ermitetest | |||
| 345 | + | `) | |||
| 346 | + | if err != nil { | |||
| 347 | + | t.Fatal(err) | |||
| 348 | + | } | |||
| 349 | + | t.Log("\n" + r.out + r.errOut) | |||
| 350 | + | for _, w := range []string{"binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc", "status: enabled\n", "flags: F\n", "ran through binfmt_misc\n"} { | |||
| 351 | + | if !strings.Contains(r.out, w) { | |||
| 352 | + | t.Errorf("output lacks %q", w) | |||
| 353 | + | } | |||
| 354 | + | } | |||
| 355 | + | } | |||