From 3e92765939804769ad8bf85b935d955beb31676c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Sat, 2 May 2026 08:15:49 -0400 Subject: [PATCH] storage_zfs: idempotently re-mount containers in start and export paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #18: ZFS clones are created with canmount=noauto so unprivileged callers don't trigger CAP_SYS_ADMIN-gated automount at create time, but the same property makes `zfs mount -a` skip them at boot. After a reboot, datasets exist (so `enroot list` still names them) but their mountpoints are empty, and `enroot start` failed with "No such file or directory" until the user ran `zfs mount` by hand. Add a small idempotent zfs::ensure_container_mounted helper that calls enroot-zfs-mount on the named dataset (silent no-op on already-mounted) and invoke it from runtime::start and runtime::_export_sqsh before each function path-walks the rootfs. Templates already get explicit mount calls in their lifecycle helpers; user containers and exported sqsh sources were the gap. Verified on spark-ctrl with a simulated reboot (zfs unmount of a live container's dataset): pre-fix `enroot start` errored, post-fix it remounts and runs. Idempotent on already-mounted datasets, error path on a non-existent name still surfaces a clean "No such file or directory" rather than a zfs internal error. Signed-off-by: Patrick Sodré --- src/runtime.sh | 14 ++++++++++++++ src/storage_zfs.sh | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/runtime.sh b/src/runtime.sh index efa4e35..449de29 100644 --- a/src/runtime.sh +++ b/src/runtime.sh @@ -302,6 +302,13 @@ runtime::start() { if [[ "${rootfs}" == */* ]]; then common::err "Invalid argument: ${rootfs}" fi + # ZFS containers are created with canmount=noauto, so a freshly- + # rebooted host has the dataset present but unmounted. Mount it + # idempotently before the directory existence check below — the + # cap helper silently no-ops on already-mounted datasets. + if zfs::enabled; then + zfs::ensure_container_mounted "${rootfs}" + fi rootfs=$(common::realpath "${ENROOT_DATA_PATH}/${rootfs}") if [ ! -d "${rootfs}" ]; then common::err "No such file or directory: ${rootfs}" @@ -689,6 +696,13 @@ runtime::_export_sqsh() { common::checkcmd mksquashfs + # ZFS containers may be unmounted post-reboot (canmount=noauto + no + # boot-time auto-mount); ensure the dataset is mounted before + # mksquashfs walks the rootfs path. + if zfs::enabled; then + zfs::ensure_container_mounted "${rootfs_name}" + fi + # Resolve the container rootfs path. rootfs=$(common::realpath "${ENROOT_DATA_PATH}/${rootfs_name}") if [ ! -d "${rootfs}" ]; then diff --git a/src/storage_zfs.sh b/src/storage_zfs.sh index 01a327a..ef04b67 100644 --- a/src/storage_zfs.sh +++ b/src/storage_zfs.sh @@ -385,6 +385,22 @@ zfs::clone_container() { fi } +# Idempotent-mount a named container's ZFS dataset. ZFS clones are created +# with canmount=noauto so unprivileged callers don't trigger mount(2) +# (which needs CAP_SYS_ADMIN) at create time — but that also makes +# zfs mount -a skip them at boot, leaving the container's mountpoint +# empty after a reboot. Call this before any path-based access of a +# named container (start, export-sqsh, …) so the rootfs becomes visible +# again. enroot-zfs-mount silently no-ops on already-mounted datasets, +# so this is safe to call unconditionally. Best-effort: an absent +# dataset is silently ignored; the caller will surface a clearer +# "no such directory" error a moment later when the existence check +# fails. +zfs::ensure_container_mounted() { + local -r name="$1" + enroot-zfs-mount "$(zfs::store_dataset)/${name}" 2> /dev/null || : +} + # Destroys a user container. The clone's origin template is left in place; # its lifecycle is owned by zfs::sweep_templates (warm/cold/pressure-driven # eviction on next create), so a remove + re-create cycle within