From 4de1680ffacbb255d3204d68b679785a1a3a35f3 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 9 Sep 2026 08:38:47 +0900 Subject: [PATCH 1/3] Fix socket activation with systemd v258 and later systemd v258 introduced $LISTEN_PIDFDID, which is set along with $LISTEN_PID so as to protect the file descriptor passing against PID reuse. sd_listen_fds(3) ignores the passed file descriptors when $LISTEN_PIDFDID does not correspond to the calling process. The activation helper was rewriting $LISTEN_PID to its own PID but leaving $LISTEN_PIDFDID pointing to the original process, so the target command received no file descriptor at all: $ systemd-socket-activate -l /tmp/uuidd.sock \ rootlesskit uuidd --no-pid --no-fork --socket-activation uuidd: no file descriptors received, check systemctl status uuidd.socket Rewrite $LISTEN_PIDFDID together with $LISTEN_PID. When the pidfd inode ID cannot be determined (needs Linux 6.9 or later), just unset the variable so that the target command falls back to checking $LISTEN_PID only. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- pkg/systemd/activation/activation.go | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pkg/systemd/activation/activation.go b/pkg/systemd/activation/activation.go index 19d1a50f..b9cca73b 100644 --- a/pkg/systemd/activation/activation.go +++ b/pkg/systemd/activation/activation.go @@ -5,6 +5,8 @@ import ( "os/exec" "strconv" "syscall" + + "golang.org/x/sys/unix" ) type Opt struct { @@ -16,6 +18,7 @@ func ActivationHelper(opt Opt) error { pid := os.Getpid() os.Unsetenv(opt.RunActivationHelperEnvKey) os.Setenv("LISTEN_PID", strconv.Itoa(pid)) + fixListenPidfdID() argsv := opt.TargetCmd execPath, err := exec.LookPath(argsv[0]) if err != nil { @@ -26,3 +29,38 @@ func ActivationHelper(opt Opt) error { } panic("should not reach here") } + +// fixListenPidfdID updates $LISTEN_PIDFDID to the pidfd inode ID of the current +// process. $LISTEN_PIDFDID is set by systemd v258 and later, in addition to +// $LISTEN_PID, so as to protect the file descriptor passing against PID reuse. +// sd_listen_fds(3) ignores the file descriptors when $LISTEN_PIDFDID does not +// correspond to the calling process, so it has to be rewritten together with +// $LISTEN_PID. +func fixListenPidfdID() { + const key = "LISTEN_PIDFDID" + if _, ok := os.LookupEnv(key); !ok { + return + } + id, err := selfPidfdID() + if err != nil { + // Unique pidfd inode IDs need Linux 6.9 or later. + // Just unset the variable so that the target command falls back to + // checking $LISTEN_PID only. + os.Unsetenv(key) + return + } + os.Setenv(key, strconv.FormatUint(id, 10)) +} + +func selfPidfdID() (uint64, error) { + fd, err := unix.PidfdOpen(os.Getpid(), 0) + if err != nil { + return 0, err + } + defer unix.Close(fd) + var st unix.Stat_t + if err := unix.Fstat(fd, &st); err != nil { + return 0, err + } + return st.Ino, nil +} From 31df5660d4075daafd6520b0d8e3b4c2d7f50e57 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 9 Sep 2026 09:21:32 +0900 Subject: [PATCH 2/3] CI: avoid AppArmor-confined nslookup with --detach-netns Ubuntu 25.10 and later ship an AppArmor profile for /usr/bin/nslookup (bind9-dnsutils). The profile includes abstractions/nameservice-strict, which allows reading /etc/resolv.conf and the well-known resolv.conf paths under /run, but nothing else. With --detach-netns, RootlessKit does not bind-mount its own resolv.conf to /etc/resolv.conf, as the child command runs in the host's network namespace and has to keep following the DNS configuration of the host. So /etc/resolv.conf is left as the copy-up symlink, and --copy-up=/run redirects its target /run/systemd/resolve/stub-resolv.conf to /run/.roXXXXXXXXXX/systemd/resolve/ stub-resolv.conf, which the profile does not allow: $ rootlesskit --net=slirp4netns --copy-up=/etc --copy-up=/run \ --detach-netns -- nslookup example.com nslookup: parse of /etc/resolv.conf failed openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY) = -1 EACCES Use busybox's nslookup applet for --detach-netns, as busybox is not confined by AppArmor (its profile is flags=(unconfined)). The non-detached tests keep using bind9's nslookup, so that they still cover the AppArmor compatibility of the bind-mounted /etc/resolv.conf. Also document the caveat in docs/network.md. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- docs/network.md | 11 +++++++++++ hack/integration-net.sh | 13 ++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/network.md b/docs/network.md index ba3236e5..e777c3b2 100644 --- a/docs/network.md +++ b/docs/network.md @@ -269,6 +269,17 @@ and executes the child command in the host's network namespace. The child command can enter `$ROOTLESSKIT_STATE_DIR/netns` by itself to create nested network namespaces. +As the child command runs in the host's network namespace, `$ROOTLESSKIT_STATE_DIR/resolv.conf` is +**not** bind-mounted to `/etc/resolv.conf`, so that the child command keeps following the DNS +configuration of the host. + +A caveat is that `--copy-up=/run` redirects `/run/systemd` (and the other entries of `/run`) to the +copy-up directory `/run/.roXXXXXXXXXX`. When `/etc/resolv.conf` on the host is a symlink to a file +under `/run`, e.g., systemd-resolved's `/run/systemd/resolve/stub-resolv.conf`, it is resolved to a +path under the copy-up directory inside the namespace. +This confuses AppArmor-confined programs such as `nslookup` from bind9-dnsutils (Ubuntu 25.10 and +later), as their profiles only allow reading the well-known paths. + ## Build tags to omit drivers diff --git a/hack/integration-net.sh b/hack/integration-net.sh index 85a8f08f..ce582d97 100755 --- a/hack/integration-net.sh +++ b/hack/integration-net.sh @@ -18,7 +18,18 @@ if [ "${net}" = "lxc-user-nic" ]; then # ignore "lxc-net is already running" error sudo /usr/lib/$(uname -m)-linux-gnu/lxc/lxc-net start || sudo /etc/init.d/lxc-net start || true fi -$ROOTLESSKIT --net=${net} --copy-up=/etc --copy-up=/run --disable-host-loopback ${flags} -- nslookup example.com +NSLOOKUP="nslookup" +if echo "${flags}" | grep -q -- --detach-netns; then + # With --detach-netns, RootlessKit does not bind-mount its own resolv.conf to + # /etc/resolv.conf, so /etc/resolv.conf is left as the copy-up symlink. + # When /etc/resolv.conf on the host points to a file under /run (e.g., + # systemd-resolved's stub-resolv.conf), --copy-up=/run redirects it into the + # copy-up directory (/run/.roXXXXXXXXXX/...), which is not allowed by the + # AppArmor profile of nslookup from bind9-dnsutils (Ubuntu 25.10 and later). + # busybox is not confined by AppArmor, so use its nslookup applet instead. + NSLOOKUP="busybox nslookup" +fi +$ROOTLESSKIT --net=${net} --copy-up=/etc --copy-up=/run --disable-host-loopback ${flags} -- ${NSLOOKUP} example.com # Test that a server process listening on the host loopback is not accessible from the isolated namespace tmp=$(mktemp -d) From f758760e76e899cd3937a82e22fbcf69c0b6cbde Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 9 Sep 2026 08:01:53 +0900 Subject: [PATCH 3/3] CI: update Ubuntu (26.04) Signed-off-by: Akihiro Suda --- .github/workflows/main.yaml | 14 +++++++------- .github/workflows/release.yaml | 2 +- Dockerfile | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1a49d45e..caad7421 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -3,7 +3,7 @@ on: [push, pull_request] jobs: lint: name: "Lint" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: "Check out" uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -17,7 +17,7 @@ jobs: version: v2.13.2 test-unit: name: "Unit test" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: "Check out" uses: actions/checkout@v7 @@ -27,7 +27,7 @@ jobs: run: docker run --rm --privileged rootlesskit:test-unit test-unit-iptables-fallback: name: "Unit test (source-ip-transparent iptables fallback, no nft)" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: "Check out" uses: actions/checkout@v7 @@ -37,14 +37,14 @@ jobs: run: docker run --rm --privileged rootlesskit:test-unit-iptables-fallback test-cross: name: "Cross compilation test" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v7 - name: "Build binaries" run: DOCKER_BUILDKIT=1 docker build -o /tmp/artifact --target cross-artifact . test-integration: name: "Integration test" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: "Set up AppArmor" run: | @@ -265,7 +265,7 @@ jobs: test-integration-docker: name: "Integration test (Docker)" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 strategy: fail-fast: false matrix: @@ -339,7 +339,7 @@ jobs: build-tags: name: "Build with disabled drivers tags" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: "Check out" uses: actions/checkout@v7 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0eecbc40..12b25bd1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,7 +16,7 @@ on: jobs: release: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 # The maximum access is "read" for PRs from public forked repos # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token permissions: diff --git a/Dockerfile b/Dockerfile index 511f3d90..6b9ff2d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG GO_VERSION=1.26 -ARG UBUNTU_VERSION=24.04 +ARG UBUNTU_VERSION=26.04 ARG SHADOW_VERSION=4.20.2 ARG SLIRP4NETNS_VERSION=v1.3.5 ARG VPNKIT_VERSION=0.6.0 @@ -39,7 +39,7 @@ CMD ["go","test","-v","-race","github.com/rootless-containers/rootlesskit/..."] # idmap runnable without --privileged (but still requires seccomp=unconfined apparmor=unconfined) FROM ubuntu:${UBUNTU_VERSION} AS idmap ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y automake autopoint bison gettext git gcc libbsd-dev libcap-dev libtool make pkg-config +RUN apt-get update && apt-get install -y automake autopoint bison gettext git gcc libbsd-dev libcap-dev libcrypt-dev libtool make pkg-config RUN git clone https://github.com/shadow-maint/shadow.git /shadow WORKDIR /shadow ARG SHADOW_VERSION @@ -61,7 +61,7 @@ RUN make && make install FROM ubuntu:${UBUNTU_VERSION} AS test-integration # iproute2: for `ip` command that rootlesskit needs to exec -# liblxc-common and lxc-utils: for `lxc-user-nic` binary required for --net=lxc-user-nic +# lxc: for `lxc-user-nic` binary required for --net=lxc-user-nic # iperf3: only for benchmark purpose # busybox: only for debugging purpose # sudo: only for lxc-user-nic benchmark and rootful veth benchmark (for comparison) @@ -70,7 +70,7 @@ FROM ubuntu:${UBUNTU_VERSION} AS test-integration # systemd and uuid-runtime: for systemd-socket-activate used by integration-systemd-socket.sh # iptables: for Docker (dockerd-rootless itself still uses iptables). # nftables: for source-ip-transparent (rootlesskit's own builtin port driver). -RUN apt-get update && apt-get install -y iproute2 liblxc-common lxc-utils iperf3 busybox sudo libcap2-bin curl bind9-dnsutils systemd uuid-runtime iptables nftables +RUN apt-get update && apt-get install -y iproute2 lxc iperf3 busybox sudo libcap2-bin curl bind9-dnsutils systemd uuid-runtime iptables nftables COPY --from=idmap /usr/bin/newuidmap /usr/bin/newuidmap COPY --from=idmap /usr/bin/newgidmap /usr/bin/newgidmap RUN /sbin/setcap cap_setuid+eip /usr/bin/newuidmap && \