From 51b7262b724205280ba85f8c59acdde9412597a1 Mon Sep 17 00:00:00 2001 From: magges Date: Sat, 11 Jul 2026 23:06:53 +0000 Subject: [PATCH] Fix five real bugs found by actually installing and running the plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First live end-to-end install on real Unraid hardware (all 8 built packages installed via upgradepkg, rc.podman started, containers pulled/run/networked/port-mapped) — surfaced five genuine bugs no amount of container-based CI testing could have caught, since none of them exist inside the vbatts/slackware:15.0 build container: 1. rc.podman never created $PODMAN_LOG_DIR before redirecting the podman system service's output into it, so the service failed to even start ("No such file or directory"). Added it alongside the existing PODMAN_RUN_DIR mkdir. 2. config/storage.conf hardcoded a [storage] table, and podman-config.sh's `sync` step appended a second one at boot with the real graphroot/runroot — TOML forbids defining the same table twice. Removed the template's [storage] entirely; sync already generates the whole thing. 3. config/policy.json had a "_comment" pseudo-field for documentation, but containers/image's policy parser rejects any unknown top-level key outright. JSON has no comment syntax; moved the rationale into docs/ARCHITECTURE.md instead. 4. netavark >= 2.0 dropped its iptables firewall driver entirely (verified: passing "iptables" is flatly rejected) — nftables or firewalld are the only remaining options, and firewalld needs systemd/dbus, which Unraid has neither of. Set firewall_driver = "nftables" explicitly and documented that Unraid OS doesn't ship the `nft` binary this needs (a slackware64 nftables package works; not yet wired into the build/install pipeline — see follow-up). 5. Every container failed with "crun: pivot_root: Invalid argument". Root cause: Unraid's / is permanently the kernel's initial "rootfs" pseudo-filesystem (Unraid never pivots to a real one at boot — the whole OS runs from RAM), and pivot_root(2) unconditionally rejects that as the old root. This is not new: Docker/runc hits the exact same kernel restriction on this exact host and silently falls back to an MS_MOVE-based chroot; crun has no such fallback, only a --no-pivot flag with no config-file equivalent. Added plugin/sbin/crun-no-pivot.sh, a thin wrapper that scans crun's full argument list (podman puts global flags before the subcommand, so the subcommand isn't reliably $1) and injects --no-pivot right after create/run, and pointed containers.conf's crun runtime at it. Also fixed the podman.plg postinstall's chmod glob (`podman-*.sh` -> `*.sh`), which would have skipped this new non-podman-prefixed sbin script. Verified end-to-end on the real host: pull, run, real network connectivity (wget through the container's bridge), and a published port actually serving HTTP (curl through -p 8099:80 to nginx) all work. --no-pivot's security tradeoff (disabling one particular container-escape mitigation) was explicitly discussed with and approved by the user before committing, given it must be the default for any container to start at all on this platform. Follow-up not yet done: nftables (needed for #4) is not yet a packages/ component in the reproducible build pipeline — it was only installed manually on the test host for this verification run. Co-Authored-By: Claude Sonnet 5 --- config/containers.conf | 28 +++++++++++++++++++++++++-- config/policy.json | 1 - config/storage.conf | 15 +++++++-------- docs/ARCHITECTURE.md | 12 ++++++++++++ plugin/podman.plg | 2 +- plugin/rc.d/rc.podman | 2 +- plugin/sbin/crun-no-pivot.sh | 37 ++++++++++++++++++++++++++++++++++++ 7 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 plugin/sbin/crun-no-pivot.sh diff --git a/config/containers.conf b/config/containers.conf index 1400990..b4e9358 100644 --- a/config/containers.conf +++ b/config/containers.conf @@ -18,6 +18,30 @@ # TODO: static_dir / volume_path / runroot overrides pointing at the cache-pool # backed storage location instead of RAM-root defaults. +[engine.runtimes] +# Unraid's / is the kernel's initial 'rootfs' pseudo-filesystem — Unraid +# never pivots to a real one during boot, the whole OS runs from RAM — and +# pivot_root(2) unconditionally rejects that as the "old root" (EINVAL). +# runc (what Docker uses) silently falls back to an MS_MOVE-based chroot +# in that situation; crun has no such fallback, only a --no-pivot flag on +# `create`/`run` with no config-file equivalent — so podman is pointed at +# a thin wrapper (installed by the unraid-podman package, see +# plugin/sbin/crun-no-pivot.sh) that injects it, instead of crun directly. +# Verified live: without this, every container fails with +# "crun: pivot_root: Invalid argument: OCI runtime error". +crun = ["/usr/local/sbin/crun-no-pivot.sh"] + [network] -# TODO: default network backend (netavark), default subnet range distinct from -# Docker's docker0 range — see docs/ARCHITECTURE.md section 8. +# TODO: default subnet range distinct from Docker's docker0 range — see +# docs/ARCHITECTURE.md section 8. +# +# netavark >= 2.0 dropped its iptables firewall driver entirely — only +# nftables and firewalld remain (verified against the actual netavark +# binary; "iptables" is rejected with "Must provide a valid firewall +# backend"). firewalld needs systemd/dbus, which Unraid has neither of, so +# nftables (netavark's own default — explicit here so that stays true even +# if netavark's default ever changes) is the only viable driver. Unraid OS +# does not ship the `nft` binary this needs — see docs/ARCHITECTURE.md +# section 8 for how it's provisioned. +network_backend = "netavark" +firewall_driver = "nftables" diff --git a/config/policy.json b/config/policy.json index 94a2041..c07fa99 100644 --- a/config/policy.json +++ b/config/policy.json @@ -1,5 +1,4 @@ { - "_comment": "Default container image signature verification policy. Placeholder: accepts all images without signature verification, matching Docker's default trust model on Unraid today. See docs/ARCHITECTURE.md section 10 (Images). Revisit before a 1.0 release if signed-image verification becomes a goal.", "default": [ { "type": "insecureAcceptAnything" } ], diff --git a/config/storage.conf b/config/storage.conf index 0ce7e66..8f60440 100644 --- a/config/storage.conf +++ b/config/storage.conf @@ -7,14 +7,13 @@ # # Full reference: https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md -[storage] -driver = "overlay" -# runroot and graphroot are set at runtime by rc.podman based on -# /boot/config/plugins/podman/podman.cfg (configurable storage location), -# not hardcoded here. TODO: document the exact substitution mechanism once -# rc.podman is implemented. -# graphroot = "/var/lib/containers/storage" -# runroot = "/var/run/containers/storage" +# The [storage] table itself (driver, graphroot, runroot) is intentionally +# NOT defined here — podman-config.sh's `sync` command appends it in full +# at sync time, since graphroot depends on STORAGE_PATH from +# /boot/config/plugins/podman/podman.cfg (configurable storage location) and +# can't be known statically. A second [storage] table here would be a TOML +# duplicate-key error once sync appends its own — see podman-config.sh's +# cmd_sync for the generated content. [storage.options] # TODO: overlay-specific mount options once the loopback filesystem diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 33cf77b..df4692e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -339,6 +339,12 @@ Unraid-Plugins. ## 8. Netzwerke - **Backend**: `netavark` + `aardvark-dns` (Podman-Default seit 4.x), kein CNI in Phase 1. +- **Firewall-Treiber**: `netavark` >= 2.0 hat seinen `iptables`-Treiber ersatzlos entfernt — + nur noch `nftables` oder `firewalld` (Letzteres braucht systemd/dbus, hat Unraid nicht). + `nftables` ist also zwingend, aber Unraid OS bringt das `nft`-Binary selbst nicht mit + (nur das ältere `iptables`/`iptables-nft`) — muss vom Plugin bereitgestellt werden + (verifiziert per Live-Test: `podman run` mit Port-Publishing scheitert ohne `nft` + mit „Must provide a valid firewall backend“). - **Default-Bridge**: eigene Bridge `podman0` (nicht `docker0`), eigener privater Adressraum (konfigurierbar, Default-Vorschlag außerhalb von Dockers Default-Range, um Kollisionen bei Parallelbetrieb zu vermeiden). @@ -397,6 +403,12 @@ Unraid-Plugins. - **Community-Applications-Kompatibilität**: kein automatischer Import von Docker-Templates in Phase 1 (eigenes, separates Vorhaben); Podman-Images werden zunächst über CLI/eigene, minimale Template-Definition verwaltet. +- **Signatur-Policy** (`config/policy.json`): `insecureAcceptAnything` als Default — + akzeptiert Images ohne Signaturprüfung, entspricht Dockers heutigem + Standard-Vertrauensmodell auf Unraid. Vor einem 1.0-Release erneut bewerten, falls + signierte Images ein Ziel werden. Die Datei selbst darf keine Kommentarfelder + enthalten (`containers/image`s Policy-Parser lehnt unbekannte Top-Level-Keys wie + `_comment` strikt ab) — Begründung lebt deshalb hier, nicht in der Datei. --- diff --git a/plugin/podman.plg b/plugin/podman.plg index 7224094..6da140b 100644 --- a/plugin/podman.plg +++ b/plugin/podman.plg @@ -274,7 +274,7 @@ set -u echo "Setting permissions..." chmod 0755 /etc/rc.d/rc.podman -chmod 0755 /usr/local/sbin/podman-*.sh +chmod 0755 /usr/local/sbin/*.sh chmod 0755 /usr/local/emhttp/plugins/podman/event/disks_mounted chmod 0755 /usr/local/emhttp/plugins/podman/event/stopping diff --git a/plugin/rc.d/rc.podman b/plugin/rc.d/rc.podman index 8d0c89a..662bbc9 100755 --- a/plugin/rc.d/rc.podman +++ b/plugin/rc.d/rc.podman @@ -98,7 +98,7 @@ podman_start() { # a shared process gives consistent state and event streaming. # --time=0 disables the idle-shutdown timeout (this is a long-running # daemon under our process management, not an on-demand activation). - mkdir -p "$PODMAN_RUN_DIR" + mkdir -p "$PODMAN_RUN_DIR" "$PODMAN_LOG_DIR" podman_log "start: starting podman system service on unix://$PODMAN_SOCKET" nohup podman system service --time=0 "unix://$PODMAN_SOCKET" \ > "$PODMAN_LOG_DIR/podman-service.log" 2>&1 & diff --git a/plugin/sbin/crun-no-pivot.sh b/plugin/sbin/crun-no-pivot.sh new file mode 100644 index 0000000..cd41b28 --- /dev/null +++ b/plugin/sbin/crun-no-pivot.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# ============================================================================= +# plugin/sbin/crun-no-pivot.sh +# +# Thin OCI-runtime wrapper around crun, injecting --no-pivot on `create`/ +# `run`. Unraid's / is the kernel's initial "rootfs" pseudo-filesystem — +# Unraid never pivots to a real one during boot, the whole OS runs from +# RAM — and pivot_root(2) unconditionally rejects that as the "old root" +# (EINVAL). runc (what Docker uses) silently falls back to an MS_MOVE-based +# chroot in that situation; crun has no such fallback and no config-file +# equivalent, only this per-invocation flag — so config/containers.conf +# points podman's crun runtime at this wrapper instead of crun directly. +# See docs/ARCHITECTURE.md section 8. +# +# Verified live: without this, every container fails with +# "crun: pivot_root: Invalid argument: OCI runtime error". +# +# --no-pivot is only a valid flag on crun's create/run subcommands (see +# `crun run --help`) — every other subcommand (delete, exec, kill, list, +# ...) is passed straight through unmodified. podman invokes crun with its +# own global flags BEFORE the subcommand (e.g. +# `crun --log-format=json --log create --bundle ...`), so +# the subcommand is not reliably $1 — scan every argument instead of only +# checking the first one, and insert --no-pivot immediately after +# create/run wherever it actually appears. +# ============================================================================= + +args=() +found=0 +for arg in "$@"; do + args+=("$arg") + if [ "$found" -eq 0 ] && { [ "$arg" = create ] || [ "$arg" = run ]; }; then + args+=("--no-pivot") + found=1 + fi +done +exec /usr/bin/crun "${args[@]}"