Skip to content

ephemeral: Do container namespace setup in bcvk instead of bwrap - #329

Closed
yeetypete wants to merge 5 commits into
bootc-dev:mainfrom
yeetypete:feat/drop-bwrap
Closed

yeetypete wants to merge 5 commits into
bootc-dev:mainfrom
yeetypete:feat/drop-bwrap

Conversation

@yeetypete

@yeetypete yeetypete commented Aug 23, 2026 •

Copy link
Copy Markdown

Closes: #7

Target images currently have to ship bubblewrap and bash. entrypoint.sh builds the hybrid root and then execs bwrap, which unshares, binds the API filesystems, calls pivot_root and becomes PID 1.

bcvk can do this itself: podman already provides the PID namespace with bcvk as its init, so the rest is replaced by a lightweight namespace setup in the container entrypoint. The dependency on both bubblewrap and bash go away with removing entrypoint.sh.

bcvk now runs in the image's userspace so it needs to be linked statically to work on older distros like stream9 with an older libc. Thoughts on this change?

This also fixes running Ubuntu 26.04 hosts. This was actually the original reason I looked into removing bubblewrap. On 26.04 the bwrap-userns-restrict AppArmor profile denies capabilities to bwrap's children. virtiofsd exits at startup with "can't apply the child capabilities" and bcvk polls for SSH until it times out. This might also be related to #306.

Tested by running all integration tests (just test-integration) on an ubuntu 26.04 host. Note that test_to_base_disk_integration_with_list fails with:

FAIL [   0.326s] integration-tests::integration-tests test_to_base_disk_integration_with_list
  stdout ───

    running 1 test
    Testing to-base-disk integration with base-disks list
    Initial base disk count: 12
    to-base-disk output: Created base disk: /home/psiegel/.local/share/libvirt/images/bootc-base-edda9015d6f14aa4.qcow2
    Final base-disks list:
    ┌───────────────────────────────────┬──────────┬──────┬──────────────────┬──────────────────────────────────────────────────────────┐
    │ NAME                              ┆ SIZE     ┆ REFS ┆ CREATED          ┆ IMAGE DIGEST                                             │
    ╞═══════════════════════════════════╪══════════╪══════╪══════════════════╪══════════════════════════════════════════════════════════╡
    │ bootc-base-7b23c3ceb66bf50c.qcow2 ┆ 1.69 GiB ┆ 0    ┆ 2026-08-23 01:12 ┆ sha256:6673fbbd49b11314700b0e842ed75149a673eb9faf7ec4... │
    ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
    │ bootc-base-edda9015d6f14aa4.qcow2 ┆ 1.55 GiB ┆ 0    ┆ 2026-08-23 01:20 ┆ sha256:6673fbbd49b11314700b0e842ed75149a673eb9faf7ec4... │
    ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
    │ bootc-base-92414e97c3abaecc.qcow2 ┆ 1.55 GiB ┆ 0    ┆ 2026-08-23 01:22 ┆ sha256:6673fbbd49b11314700b0e842ed75149a673eb9faf7ec4... │
    ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
    │ bootc-base-487758b3396b22bb.qcow2 ┆ 1.69 GiB ┆ 0    ┆ 2026-08-23 01:10 ┆ sha256:6673fbbd49b11314700b0e842ed75149a673eb9faf7ec4... │
    ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
    │ bootc-base-7ac29c1405a4366c.qcow2 ┆ 1.69 GiB ┆ 0    ┆ 2026-08-23 01:08 ┆ sha256:6673fbbd49b11314700b0e842ed75149a673eb9faf7ec4... │
    └───────────────────────────────────┴──────────┴──────┴──────────────────┴──────────────────────────────────────────────────────────┘

    Found 5 base disks
    test test_to_base_disk_integration_with_list ... FAILED

    failures:

    ---- test_to_base_disk_integration_with_list ----
    test panicked: Base disk count should increase after creation

    failures:
        test_to_base_disk_integration_with_list

    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 89 filtered out; finished in 0.32s

  stderr ───

    thread '<unnamed>' (3280798) panicked at crates/integration-tests/src/tests/libvirt_to_base_disk.rs:205:9:
    Base disk count should increase after creation
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

But this looks unrelated.

Also tested together with https://github.com/yeetypete/bootc-ubuntu where this fixes a previous hang due to the AppArmor issue.

Assisted-by: AI

@yeetypete

Copy link
Copy Markdown
Author

@cgwalters thoughts? I see in #7 you mention trying out a different approach.

@cgwalters

Copy link
Copy Markdown
Collaborator

The tradeoff here is "link bcvk statically". Perhaps we could just have a little helper binary that's statically linked? Though I guess mechanically the "bcvk is a single binary" thing would need to be solved by actually bundling the inner statically linked binary as an ELF segment in the outer binary or so...this came up in #259 too.

There's some corner cases here, like I am not totally sure this will work if we're doing e.g. cross-arch qemu emulation, as we'd be mixing a native arch binary into a foreign arch root.

But...yeah on the positive side it's really nice to move what's currently in shell script into Rust.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Moves ephemeral container namespace setup from bubblewrap/bash into bcvk, addressing AppArmor compatibility and reducing target-image dependencies.

Changes:

  • Adds Rust-based hybrid-root and namespace initialization.
  • Reworks container launch and status monitoring around the static bcvk executable.
  • Removes the shell/bubblewrap entrypoint.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Makefile Builds and installs a statically linked binary.
docs/src/installation.md Updates target-image requirements.
crates/kit/src/sandbox.rs Implements namespace and root setup.
crates/kit/src/run_ephemeral.rs Launches bcvk directly as container entrypoint.
crates/kit/src/run_ephemeral_ssh.rs Uses bcvk for status monitoring.
crates/kit/src/main.rs Runs sandbox setup before Tokio initialization.
crates/kit/scripts/entrypoint.sh Removes the former bubblewrap entrypoint.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/kit/src/sandbox.rs
Comment thread Makefile
Comment on lines +17 to +18
RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target $(CARGO_BUILD_TARGET)
install -D -m755 target/$(CARGO_BUILD_TARGET)/release/bcvk target/release/bcvk
Comment thread docs/src/installation.md
Comment on lines 35 to 37
For `bcvk ephemeral` operations, the bootc container images you run must contain:
- systemctl (systemd)
- objcopy (binutils)
- bwrap (bubblewrap)
- ssh, ssh-keygen (openssh-clients)
Comment thread crates/kit/src/sandbox.rs
Comment on lines +30 to +34
/// Assemble the hybrid root and make it this process's root.
///
/// Only the VM supervisor calls this. Anything else arrives later through
/// `podman exec`, which joins the supervisor's namespaces and so is already in
/// the hybrid root.
Comment thread Makefile

all: bin manpages

# bcvk is bind-mounted and ran inside the container it starts. We build it statically
@yeetypete

Copy link
Copy Markdown
Author

Though I guess mechanically the "bcvk is a single binary" thing would need to be solved by actually bundling the inner statically linked binary as an ELF segment in the outer binary or so...this came up in #259 too.

There's some corner cases here, like I am not totally sure this will work if we're doing e.g. cross-arch qemu emulation, as we'd be mixing a native arch binary into a foreign arch root.

Yes would be nice if this can be made to support qemu emulation too as i'd also like to be able to do this with bcvk in the future. Let me investigate a bit more how hard it would be to make this work more generally.

Comment thread crates/kit/src/sandbox.rs
.unwrap_or_default()
}

/// Populate /etc/passwd in the hybrid root, which ssh-keygen wants to exist.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think this is some pretty ugly technical debt that ideally we could drop...needs some consideration

Comment thread Makefile
# bcvk is bind-mounted and ran inside the container it starts. We build it statically
# so that it does not depend on the image's loader or its libc. An explicit --target
# is required because proc-macros cannot be built statically.
CARGO_BUILD_TARGET := $(shell rustc -vV | sed -n 's/^host: //p')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty hacky...I think we can express our desire to statically link via build.rs probably?

@yeetypete

Copy link
Copy Markdown
Author

@cgwalters apologies for the radio silence. I'll pick this up again this week :)

@cgwalters

cgwalters commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks, I would like to get something in this form in, but it's a big change and needs some careful consideration.

Requiring bubblewrap in the target image is not obvious to users, and
several distributions' images do not carry it. bwrap was providing an
unshare, a handful of bind mounts, a pivot_root and a PID namespace.
Podman already gives the container a PID namespace with bcvk as its
init, so what remains is a short piece of rustix.

The entrypoint script moves under /run, because `podman exec` joins the
namespace bcvk now runs in, where /var/lib/bcvk is not visible.

Closes: bootc-dev#7
Assisted-by: AI
Signed-off-by: Peter Siegel <psiegel2000@icloud.com>
The script assembled /run/tmproot, read the image's systemd version and
exec'd bcvk. Doing this in Rust allows us to stop depending on bash in a
target image.

Assisted-by: AI
Signed-off-by: Peter Siegel <psiegel2000@icloud.com>
…lation readme.

Signed-off-by: Peter Siegel <psiegel2000@icloud.com>
bcvk is bind-mounted into the container it starts and runs there before
the entrypoint switches to the host's /usr. A dynamic build must load
against the image's glibc, which fails when the image ships an older one
than the build host, such as centos-bootc:stream9. A static build has no
such dependency.

Assisted-by: AI
Signed-off-by: Peter Siegel <psiegel2000@icloud.com>
systemd-sysusers was executed from the image's PATH before the root
change, which required the target image to ship it and to be executable
on the host architecture. The host's systemd-sysusers cannot simply be
called by path before the pivot either, as its loader and libc would
then be resolved from the image.

Run it after pivot_root instead, where / is the hybrid root: the host's
binary, its libc and its sysusers.d then produce the /etc/passwd that
the host's ssh-keygen consumes. systemctl --version remains the only
image binary executed, as it has to report the image's systemd.

Signed-off-by: Peter Siegel <psiegel2000@icloud.com>
Assisted-by: AI
@yeetypete

Copy link
Copy Markdown
Author

Ok looks like this also fixes a separate issue in latest bcvk where the backgrounded bwrap was cutting off stdin:

bwrap --as-pid-1 --unshare-pid "${BWRAP_ARGS[@]}" --bind /run /run -- ${SELFEXE} container-entrypoint "$@" &

@cgwalters cgwalters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm overall good to merge this, but I think this won't be the final form.

I am still not happy with static linking; the way more complex option of generating a tiny statically linked binary or getting the required code into something else we expect to be in the image (util-linux, systemd, bootc, crun/runc) would probably make sense.

Otherwise mostly just needs a rebase now

Comment thread crates/kit/src/sandbox.rs
// SAFETY: unshare is unsafe only for UnshareFlags::FILES, where one thread
// can be left unable to use another's file descriptors.
#[allow(unsafe_code)]
unsafe { unshare_unsafe(UnshareFlags::NEWNS) }.context("Unsharing mount namespace")?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW something to investigate...how about requiring util-linux? The thing about bwrap is we don't actually need security here just access to the Linux containerization syscalls which modern util-linux also is generally exposing?

I bet we could do this with unshare -m + mount --bind?

@yeetypete

Copy link
Copy Markdown
Author

@cgwalters static linking also messes with some bcvk rpm packaging linting as well, no? I guess i'd have to disable some lints.

One alternative idea: instead of using the image as the container root and pivoting into the hybrid root, we could try podman run --rootfs with a skeleton dir and the host's /usr mounted. The bcvk binary could use the host's libc mounted there and we wouldnt need static linking. This would also require some adjustments to bcvk ephemeral ps, etc. though because the container is no longer created from the image...

@cgwalters

Copy link
Copy Markdown
Collaborator

@cgwalters static linking also messes with some bcvk rpm packaging linting as well, no? I guess i'd have to disable some lints.

Probably...

The thing I really don't like statically linking is e.g. openssl.so - also statically linking glibc breaks NSS and such, and we've had historical problems with Go in that regard.

we could try podman run --rootfs with a skeleton dir and the host's /usr mounted.

This path is really bcvk ephemeral host I think, this came up in #73

A bcvk ephemeral host would be really cool and quite useful, but it feels in some ways like it's also stretching the boundaries of what I think this tool should do. There's a variety of prior art in the "run host as a VM" like:

It would clearly make sense to have bcvk ephemeral host if the host itself is a bootc system though! Then everything is much cleaner we can just mount the host's image.

But it'd be a breaking change to require the host to be bootc of course...

It'd be a good research spike though.

I guess an interesting topic in this is do we use the host kernel or the target bootc kernel?

I strongly believe it really needs to be the target by default - otherwise lots of "cross OS/distro" scenarios around the installation will break.

That's why bcvk to-disk builds on top of bcvk ephemeral in the way it does.

A model that skews the host userspace with the guest kernel is very likely to break in potentially obscure ways. I mean it'd often work but currently e.g. we don't require the host kernel to have CONFIG_FS_EROFS enabled for example but we definitely would in such a model.

So yes in a nutshell that's the reason for the current hacky "shell script+bwrap" to get the virt stack from the host, but otherwise use the target image.

@yeetypete

Copy link
Copy Markdown
Author

A bcvk ephemeral host would be really cool and quite useful, but it feels in some ways like it's also stretching > I guess an interesting topic in this is do we use the host kernel or the target bootc kernel?

I strongly believe it really needs to be the target by default - otherwise lots of "cross OS/distro" scenarios around the installation will break.

That's why bcvk to-disk builds on top of bcvk ephemeral in the way it does.

Thanks for the explanation. Ok I think I'll close this for now. It was an interesting experiment and for now I can work around/live with the apparmor issues on 26.04 and adding the dependencies we shell out to in the entrypoint while we converge to the right long term fix.

@yeetypete yeetypete closed this Oct 1, 2026
@cgwalters

Copy link
Copy Markdown
Collaborator

Hmm you did a lot of work here though...I guess one thing we could do in theory is make this a build-time option?

That said I'd like to do a spike on #329 (comment)

@cgwalters-bot take a quick crack at that

@cgwalters-bot

Copy link
Copy Markdown
Contributor

@cgwalters util-linux spike re #329 (comment): it works. Draft: cgwalters-forge#12

It keeps entrypoint.sh but replaces the bwrap call with unshare --mount --propagation slave --pid --fork --kill-child=SIGTERM. The script then re-invokes itself, does mount --rbind for /run, /dev and /var/tmp, mount -t proc and mount -t tmpfs /tmp under /run/tmproot, and finishes with exec chroot /run/tmproot /run/selfexe container-entrypoint ....

  • Version floor: util-linux 2.32, for --kill-child. Everything else it uses is older. c9s ships 2.37.4, c10s 2.40.2 and Fedora 43 2.41.5. It doesn't need --map-root-user, idmapped mounts or X-mount.*, because the container is already privileged (--cap-add=all, seccomp unconfined).
  • Gaps vs bwrap: it uses chroot instead of pivot_root, so the old root is still reachable; that doesn't matter when we only want the namespace and not isolation. --kill-child stands in for --die-with-parent. --as-pid-1 becomes --pid --fork plus exec. bwrap's seccomp and userns handling aren't needed here. bwrap's --tmpfs /run was overridden by the later --bind /run /run anyway, so I dropped it. It still needs a shell script plus unshare, mount and chroot in the target image (util-linux-core and coreutils, which every bootc image has). ephemeral: Do container namespace setup in bcvk instead of bwrap #329 removes the script and all those image dependencies; this spike doesn't.
  • Size: +41/-39 lines, nearly all in entrypoint.sh (+33/-31; the rest is docs and comments). ephemeral: Do container namespace setup in bcvk instead of bwrap #329 is +206/-135, including the new 156-line sandbox.rs.
  • Testing: done on a devspace with rootless podman only; rootful is untested. bcvk ephemeral run-ssh passed on c9s, c10s, Fedora 43 and on a c10s image with bubblewrap removed. The integration suite gave 90 passed and 3 failed. Two of those failures also happen on main on that host (port 9090 is in use there, and a base-disk count check). The third passes when rerun alone.

Generated-by: https://github.com/cgwalters/#llms

@cgwalters

Copy link
Copy Markdown
Collaborator

On 26.04 the bwrap-userns-restrict AppArmor profile denies capabilities to bwrap's children

I completely forgot to associate this, but I also ran face-first into that issue; me and my agents ended up doing https://github.com/bootc-dev/actions/blob/f77e18eca58220a5f8e82a115d65ee5973fcec83/bootc-host-setup/ci/workarounds.sh#L16

I am honestly a bit skeptical of the security properties the AA profile for bwrap provides in general I think it would literally work to bypass it by doing e.g. cp /usr/bin/bwrap /usr/bin/not-bwrap and running that binary...AppArmor is just really weak in being "pathname based" in contrast to SELinux to start.

It would also likely make sense to patch the AA profile if possible to differentate between "in init userns" vs not.

Anyways it's worth noting that CI here actually runs on 26.04 but doesn't fail precisely because we have that workaround.

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.

ephemeral: switch to systemd-in-container

4 participants