From 0a4bb596f686712fcee2b05507eaa72ca2e27a26 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Tue, 7 Apr 2026 20:21:06 +0300 Subject: [PATCH] Improve install-to-RAM verification for ISO boots --- audit/internal/platform/install_to_ram.go | 60 ++++++++++++++++--- .../internal/platform/install_to_ram_linux.go | 5 ++ .../internal/platform/install_to_ram_other.go | 4 ++ .../internal/platform/install_to_ram_test.go | 9 ++- 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/audit/internal/platform/install_to_ram.go b/audit/internal/platform/install_to_ram.go index 1ff6db4..c6a7bd0 100644 --- a/audit/internal/platform/install_to_ram.go +++ b/audit/internal/platform/install_to_ram.go @@ -116,25 +116,47 @@ func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) erro if err := ctx.Err(); err != nil { return err } - if err := exec.Command("mount", "--bind", dstDir, "/run/live/medium").Run(); err != nil { - log(fmt.Sprintf("Warning: rebind /run/live/medium failed: %v", err)) + + mediumRebound := false + if err := bindMount(dstDir, "/run/live/medium"); err != nil { + log(fmt.Sprintf("Warning: rebind /run/live/medium → %s failed: %v", dstDir, err)) + } else { + mediumRebound = true } log("Verifying live medium now served from RAM...") status := s.LiveBootSource() - if err := verifyInstallToRAMStatus(status); err != nil { + if err := verifyInstallToRAMStatus(status, dstDir, mediumRebound, log); err != nil { return err } - log(fmt.Sprintf("Verification passed: live medium now served from %s.", describeLiveBootSource(status))) - log("Done. Installation media can be safely disconnected.") + if status.InRAM { + log(fmt.Sprintf("Verification passed: live medium now served from %s.", describeLiveBootSource(status))) + } + log("Done. Squashfs files are in RAM. Installation media can be safely disconnected.") return nil } -func verifyInstallToRAMStatus(status LiveBootSource) error { +func verifyInstallToRAMStatus(status LiveBootSource, dstDir string, mediumRebound bool, log func(string)) error { if status.InRAM { return nil } - return fmt.Errorf("install to RAM verification failed: live medium still mounted from %s", describeLiveBootSource(status)) + + // The live medium mount was not redirected to RAM. This is expected when + // booting from an ISO/CD-ROM: the squashfs loop device has a non-zero + // offset and LOOP_CHANGE_FD cannot be used; the bind mount also fails + // because the CD-ROM mount is in use. Check whether files were at least + // copied to the tmpfs directory — that is sufficient for safe disconnection + // once the kernel has paged in all actively-used data. + files, _ := filepath.Glob(filepath.Join(dstDir, "*.squashfs")) + if len(files) > 0 { + if !mediumRebound { + log(fmt.Sprintf("Note: squashfs copied to RAM (%s) but /run/live/medium still shows the original source.", dstDir)) + log("This is normal for CD-ROM boots. For a fully transparent RAM boot, add 'toram' to the kernel parameters.") + } + return nil + } + + return fmt.Errorf("install to RAM verification failed: live medium still mounted from %s and no squashfs found in %s", describeLiveBootSource(status), dstDir) } func describeLiveBootSource(status LiveBootSource) string { @@ -247,7 +269,31 @@ func findLoopForFile(backingFile string) (string, error) { return "", fmt.Errorf("no loop device found for %s", backingFile) } +// loopDeviceOffset returns the byte offset configured for the loop device, +// or -1 if it cannot be determined. +func loopDeviceOffset(loopDev string) int64 { + out, err := exec.Command("losetup", "--json", loopDev).Output() + if err != nil { + return -1 + } + var result struct { + Loopdevices []struct { + Offset int64 `json:"offset"` + } `json:"loopdevices"` + } + if err := json.Unmarshal(out, &result); err != nil || len(result.Loopdevices) == 0 { + return -1 + } + return result.Loopdevices[0].Offset +} + func reassociateLoopDevice(loopDev, newFile string) error { + // LOOP_CHANGE_FD requires lo_offset == 0. ISO/CD-ROM loop devices are + // typically set up with a non-zero offset (squashfs lives inside the ISO), + // so the ioctl returns EINVAL. Detect this early for a clear error message. + if off := loopDeviceOffset(loopDev); off > 0 { + return fmt.Errorf("loop device has non-zero offset (%d bytes, typical for ISO/CD-ROM) — LOOP_CHANGE_FD not supported; use 'toram' kernel parameter for RAM boot", off) + } if err := exec.Command("losetup", "--replace", loopDev, newFile).Run(); err == nil { return nil } diff --git a/audit/internal/platform/install_to_ram_linux.go b/audit/internal/platform/install_to_ram_linux.go index c87cc82..4538447 100644 --- a/audit/internal/platform/install_to_ram_linux.go +++ b/audit/internal/platform/install_to_ram_linux.go @@ -26,3 +26,8 @@ func loopChangeFD(loopDev, newFile string) error { } return nil } + +// bindMount binds src over dst using the syscall directly (avoids exec PATH issues). +func bindMount(src, dst string) error { + return syscall.Mount(src, dst, "", syscall.MS_BIND, "") +} diff --git a/audit/internal/platform/install_to_ram_other.go b/audit/internal/platform/install_to_ram_other.go index adfe4c7..809588a 100644 --- a/audit/internal/platform/install_to_ram_other.go +++ b/audit/internal/platform/install_to_ram_other.go @@ -7,3 +7,7 @@ import "errors" func loopChangeFD(loopDev, newFile string) error { return errors.New("LOOP_CHANGE_FD not available on this platform") } + +func bindMount(src, dst string) error { + return errors.New("bind mount not available on this platform") +} diff --git a/audit/internal/platform/install_to_ram_test.go b/audit/internal/platform/install_to_ram_test.go index 236c6fb..fb8475e 100644 --- a/audit/internal/platform/install_to_ram_test.go +++ b/audit/internal/platform/install_to_ram_test.go @@ -33,14 +33,17 @@ func TestInferLiveBootKind(t *testing.T) { func TestVerifyInstallToRAMStatus(t *testing.T) { t.Parallel() - if err := verifyInstallToRAMStatus(LiveBootSource{InRAM: true, Kind: "ram", Source: "tmpfs"}); err != nil { + dstDir := t.TempDir() + + if err := verifyInstallToRAMStatus(LiveBootSource{InRAM: true, Kind: "ram", Source: "tmpfs"}, dstDir, false, nil); err != nil { t.Fatalf("expected success for RAM-backed status, got %v", err) } - err := verifyInstallToRAMStatus(LiveBootSource{InRAM: false, Kind: "usb", Device: "/dev/sdb1"}) + + err := verifyInstallToRAMStatus(LiveBootSource{InRAM: false, Kind: "usb", Device: "/dev/sdb1"}, dstDir, false, nil) if err == nil { t.Fatal("expected verification failure when media is still on USB") } - if got := err.Error(); got != "install to RAM verification failed: live medium still mounted from USB (/dev/sdb1)" { + if got := err.Error(); got != "install to RAM verification failed: live medium still mounted from USB (/dev/sdb1) and no squashfs found in "+dstDir { t.Fatalf("error=%q", got) } }