From b14417b729d2782c6605b523792d738ccde38e60 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Thu, 3 Sep 2026 15:34:30 +0300 Subject: [PATCH] fix(runtime): avoid Copy to RAM worker panic --- audit/internal/platform/install_to_ram.go | 11 +++++++++-- audit/internal/platform/install_to_ram_test.go | 4 ++++ .../2026-09-03-runtime-copy-to-ram-loop-change-fd.md | 8 ++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/audit/internal/platform/install_to_ram.go b/audit/internal/platform/install_to_ram.go index 25bfd66..bc959a4 100644 --- a/audit/internal/platform/install_to_ram.go +++ b/audit/internal/platform/install_to_ram.go @@ -401,8 +401,7 @@ func copyFileLarge(ctx context.Context, src, dst string, logFunc func(string)) e copied += int64(n) if shouldLogCopyProgress(copied, total, lastLogged) { lastLogged = copied - pct := int(float64(copied) / float64(total) * 100) - logFunc(fmt.Sprintf(" %s / %s (%d%%)", humanBytes(copied), humanBytes(total), pct)) + maybeLogCopyProgress(logFunc, copied, total) } } if err == io.EOF { @@ -415,6 +414,14 @@ func copyFileLarge(ctx context.Context, src, dst string, logFunc func(string)) e return out.Sync() } +func maybeLogCopyProgress(logFunc func(string), copied, total int64) { + if logFunc == nil || total <= 0 { + return + } + pct := int(float64(copied) / float64(total) * 100) + logFunc(fmt.Sprintf(" %s / %s (%d%%)", humanBytes(copied), humanBytes(total), pct)) +} + func shouldLogCopyProgress(copied, total, lastLogged int64) bool { if total <= 0 || copied <= 0 { return false diff --git a/audit/internal/platform/install_to_ram_test.go b/audit/internal/platform/install_to_ram_test.go index 5787379..cfffa80 100644 --- a/audit/internal/platform/install_to_ram_test.go +++ b/audit/internal/platform/install_to_ram_test.go @@ -272,3 +272,7 @@ func TestDetachInstallMedium(t *testing.T) { } }) } + +func TestMaybeLogCopyProgressAllowsNilLogger(t *testing.T) { + maybeLogCopyProgress(nil, copyProgressLogStep, copyProgressLogStep) +} diff --git a/bible-local/decisions/2026-09-03-runtime-copy-to-ram-loop-change-fd.md b/bible-local/decisions/2026-09-03-runtime-copy-to-ram-loop-change-fd.md index bfd3fa9..9c2048d 100644 --- a/bible-local/decisions/2026-09-03-runtime-copy-to-ram-loop-change-fd.md +++ b/bible-local/decisions/2026-09-03-runtime-copy-to-ram-loop-change-fd.md @@ -24,6 +24,12 @@ The Linux ABI defines `LOOP_CHANGE_FD` as `0x4C06` and `ioctlLoopChangeFD`, so every fallback reassociation called the wrong ioctl and received `EINVAL`. +The follow-up live run exposed another independent failure: copying the +remaining medium tree passes a nil progress callback to `copyFileLarge`, but +the copier called that callback unconditionally after each progress interval. +The worker therefore panicked immediately after logging "Copying remaining +medium files...", and the parent task only reported `exit status 1`. + ## Decision - Use the correct `LOOP_CHANGE_FD` request number, `0x4C06`, and protect it @@ -40,6 +46,8 @@ and received `EINVAL`. - Eject the original device without unmounting the new RAM bind mount. - Preserve a complete RAM copy after any failure that occurs after loop reassociation begins, so retry never deletes a file backing a live loop. +- Progress reporting is optional; bulk copies without a logger must never + panic. ## Consequences