From 1b8cad425e2052251554377dc88b5822a8b45944 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 01:11:20 +0000 Subject: [PATCH] USB import: load exfat/ntfs3 at boot so NTFS and exFAT sticks mount Found on the first real board: an NTFS stick was refused with "filesystem ntfs is not accepted". The helper probed for the ntfs3 driver and tried to modprobe it, but its unit runs with ProtectKernelModules=yes, so the driver never loaded and NTFS was treated as unsupported. - setup.sh and install.sh write /etc/modules-load.d/musallahboard.conf (exfat, ntfs3) and load both now; the uninstall scripts remove it - The helper always mounts NTFS with ntfs3 instead of probing and modprobing from inside its sandbox, which stays strict Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VUxfdFFUkL7SmAEUvUzspy --- docs/architecture.md | 3 ++ internal/usbimport/usbimport.go | 33 ++++----------------- internal/usbimport/usbimport_test.go | 22 ++++---------- packaging/install.sh | 11 +++++++ packaging/modules-load.d/musallahboard.conf | 5 ++++ packaging/uninstall.sh | 1 + setup.sh | 11 +++++++ uninstall.sh | 1 + 8 files changed, 44 insertions(+), 43 deletions(-) create mode 100644 packaging/modules-load.d/musallahboard.conf diff --git a/docs/architecture.md b/docs/architecture.md index a4f8c2e..22c3444 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -520,6 +520,9 @@ folder on it, and plug it into the board. - The daemon shows the update screen while it works; the stick can be removed once the screen says "Update complete". - `usb_import = false` in `agent.toml` disables it (the helper exits at once). +- The helper's sandbox (`ProtectKernelModules=yes`) cannot load filesystem + drivers, so `exfat` and `ntfs3` are loaded at boot from + `/etc/modules-load.d/musallahboard.conf`. NTFS always mounts with `ntfs3`. ### 9.7 Admin CLI diff --git a/internal/usbimport/usbimport.go b/internal/usbimport/usbimport.go index ff6f334..1b8d87f 100644 --- a/internal/usbimport/usbimport.go +++ b/internal/usbimport/usbimport.go @@ -59,8 +59,6 @@ type Helper struct { DevDir string // Exec runs a command and returns its combined output. Exec func(ctx context.Context, name string, args ...string) (string, error) - // NTFS3 reports whether the kernel's ntfs3 driver can be used. - NTFS3 func() bool // Chown gives a copied file the inbox directory's owner, so the // unprivileged daemon can read it and delete it once processed. Chown func(path, dir string) @@ -82,7 +80,6 @@ func New() *Helper { WaitTimeout: WaitTimeout, Poll: time.Second, } - h.NTFS3 = h.ntfs3Available return h } @@ -141,39 +138,21 @@ func (h *Helper) Run(ctx context.Context, name string) error { } // mountType maps blkid's type to the mount -t value, and whether it is -// accepted at all. NTFS goes through the kernel's ntfs3 driver when there is -// one; the FUSE ntfs-3g driver is not used, it runs a userspace process over -// the stick's contents. +// accepted at all. NTFS goes through the kernel's ntfs3 driver; the FUSE +// ntfs-3g driver is not used, it runs a userspace process over the stick's +// contents. The exfat and ntfs3 modules are loaded at boot +// (/etc/modules-load.d/musallahboard.conf): this helper's sandbox +// (ProtectKernelModules=yes) cannot load them itself. func (h *Helper) mountType(fsType string) (string, bool) { switch fsType { case "vfat", "exfat", "ext4", "ntfs3": return fsType, true case "ntfs": - if h.NTFS3 != nil && h.NTFS3() { - return "ntfs3", true - } + return "ntfs3", true } return "", false } -func (h *Helper) ntfs3Available() bool { - if b, err := os.ReadFile("/proc/filesystems"); err == nil && hasFS(string(b), "ntfs3") { - return true - } - _, err := h.Exec(context.Background(), "modprobe", "ntfs3") - return err == nil -} - -func hasFS(procFilesystems, name string) bool { - for _, line := range strings.Split(procFilesystems, "\n") { - f := strings.Fields(line) - if len(f) > 0 && f[len(f)-1] == name { - return true - } - } - return false -} - func (h *Helper) unmount(ctx context.Context, name, mp string) { if _, err := h.Exec(ctx, "umount", mp); err != nil { // Something still holds it (it should not); detach it anyway so a diff --git a/internal/usbimport/usbimport_test.go b/internal/usbimport/usbimport_test.go index cb8b696..50326e7 100644 --- a/internal/usbimport/usbimport_test.go +++ b/internal/usbimport/usbimport_test.go @@ -152,7 +152,6 @@ func newHelper(t *testing.T, f *fakeSys) (*Helper, *[]string) { MountRoot: filepath.Join(dir, "usb"), DevDir: "/dev", Exec: f.exec, - NTFS3: func() bool { return false }, Logf: func(format string, a ...any) { mu.Lock(); log = append(log, fmt.Sprintf(format, a...)); mu.Unlock() }, WaitTimeout: 2 * time.Second, Poll: 10 * time.Millisecond, @@ -242,22 +241,20 @@ func TestRunTimesOutWithoutDaemon(t *testing.T) { func TestRunRefusals(t *testing.T) { cases := []struct { name, dev, fs string - ntfs3 bool unsupported bool mounted string }{ - {"bad name", "mmcblk0p1", "vfat", false, false, ""}, - {"path", "../sda1", "vfat", false, false, ""}, - {"no filesystem", "sda1", "", false, true, ""}, - {"iso", "sda1", "iso9660", false, true, ""}, - {"ntfs without ntfs3", "sda1", "ntfs", false, true, ""}, - {"ntfs with ntfs3", "sda1", "ntfs", true, false, "-t ntfs3"}, + {"bad name", "mmcblk0p1", "vfat", false, ""}, + {"path", "../sda1", "vfat", false, ""}, + {"no filesystem", "sda1", "", true, ""}, + {"iso", "sda1", "iso9660", true, ""}, + {"ntfs goes through ntfs3", "sda1", "ntfs", false, "-t ntfs3"}, + {"exfat", "sda1", "exfat", false, "-t exfat"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { f := &fakeSys{fs: tc.fs, stick: map[string]int{}} h, _ := newHelper(t, f) - h.NTFS3 = func() bool { return tc.ntfs3 } err := h.Run(context.Background(), tc.dev) cmds := strings.Join(f.cmds, "\n") switch { @@ -277,10 +274,3 @@ func TestRunRefusals(t *testing.T) { }) } } - -func TestHasFS(t *testing.T) { - proc := "nodev\tsysfs\n\text4\n\tvfat\n\tntfs3\n" - if !hasFS(proc, "ntfs3") || hasFS(proc, "exfat") { - t.Fatal("hasFS") - } -} diff --git a/packaging/install.sh b/packaging/install.sh index 4105bc5..3ccdfd5 100644 --- a/packaging/install.sh +++ b/packaging/install.sh @@ -313,6 +313,17 @@ if systemd_running && command -v udevadm >/dev/null; then fi info "Installed ${UDEV_RULES[*]} in $UDEV_DIR" +# Filesystem drivers for USB sticks, loaded at boot: the USB helper's sandbox +# (ProtectKernelModules=yes) cannot load them itself. +install -d -m 0755 /etc/modules-load.d +install -o root -g root -m 0644 "${AGENT_DIR}/packaging/modules-load.d/musallahboard.conf" \ + /etc/modules-load.d/musallahboard.conf +if systemd_running; then + modprobe exfat 2>/dev/null || warn "Could not load exfat now; exFAT sticks work after a reboot" + modprobe ntfs3 2>/dev/null || warn "Could not load ntfs3 now; NTFS sticks work after a reboot" +fi +info "Installed /etc/modules-load.d/musallahboard.conf (exfat, ntfs3)" + # ── Kiosk (cage + Chromium) ─────────────────────────────────────────────────── if [[ "$WANT_KIOSK" == "yes" ]]; then section "Kiosk (cage)" diff --git a/packaging/modules-load.d/musallahboard.conf b/packaging/modules-load.d/musallahboard.conf new file mode 100644 index 0000000..c51229e --- /dev/null +++ b/packaging/modules-load.d/musallahboard.conf @@ -0,0 +1,5 @@ +# MusallahBoard: filesystem drivers for USB update sticks. Loaded at boot +# because the USB import helper runs with ProtectKernelModules=yes and cannot +# load them itself. vfat and ext4 are loaded by the base system already. +exfat +ntfs3 diff --git a/packaging/uninstall.sh b/packaging/uninstall.sh index 661c28f..4835e54 100644 --- a/packaging/uninstall.sh +++ b/packaging/uninstall.sh @@ -70,6 +70,7 @@ systemctl stop 'musallahboard-usb-import@*.service' 2>/dev/null || true # udev before the units, so a stick plugged in now starts nothing. rm -f /etc/udev/rules.d/90-musallahboard-usb.rules rm -f /etc/udev/rules.d/90-musallahboard-rtc.rules +rm -f /etc/modules-load.d/musallahboard.conf udevadm control --reload 2>/dev/null || true rm -f /lib/systemd/system/musallahboard-agent.service rm -f /lib/systemd/system/musallahboard-kiosk.service diff --git a/setup.sh b/setup.sh index dc80f94..7fc4513 100755 --- a/setup.sh +++ b/setup.sh @@ -766,6 +766,17 @@ EOF # Apply the rtc0 permissions now. Not the block subsystem: that would # start an import from a stick that happens to be plugged in. sudo udevadm trigger --subsystem-match=rtc --action=change || true + + # KEEP IN SYNC with packaging/modules-load.d/. The USB helper's sandbox + # (ProtectKernelModules=yes) cannot load filesystem drivers, so exFAT and + # NTFS sticks need them loaded at boot, and now. + sudo mkdir -p /etc/modules-load.d + printf '%s\n' \ + "# MusallahBoard: filesystem drivers for USB update sticks (see setup.sh)." \ + exfat ntfs3 | sudo tee /etc/modules-load.d/musallahboard.conf > /dev/null + sudo chmod 0644 /etc/modules-load.d/musallahboard.conf + sudo modprobe exfat 2>/dev/null || warn "Could not load exfat now; exFAT sticks work after a reboot" + sudo modprobe ntfs3 2>/dev/null || warn "Could not load ntfs3 now; NTFS sticks work after a reboot" info "USB sticks start musallahboard-usb-import@.service; rtc0 is writable by $SERVICE_USER" } diff --git a/uninstall.sh b/uninstall.sh index dbd7d8e..aae1b2c 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -66,6 +66,7 @@ section "Removing udev rules" rm -f /etc/udev/rules.d/90-musallahboard-usb.rules rm -f /etc/udev/rules.d/90-musallahboard-rtc.rules rm -f /etc/udev/rules.d/85-musallahboard-rtc.rules +rm -f /etc/modules-load.d/musallahboard.conf udevadm control --reload 2>/dev/null || true info "udev rules removed (USB import, RTC access, RTC boot-time read)"