Skip to content

fix: deep-scan findings + pre-merge review gate - #31

Merged
deveshctl merged 1 commit into
mainfrom
fix/deep-scan-2026-05
May 28, 2026
Merged

fix: deep-scan findings + pre-merge review gate#31
deveshctl merged 1 commit into
mainfrom
fix/deep-scan-2026-05

Conversation

@deveshctl

Copy link
Copy Markdown
Owner

Summary

Addresses 5 defects from the prior deep scan, plus 5 additional CONFIRMED issues surfaced by the pre-merge code-review gate (per CLAUDE.md PR review discipline rule #1).

Original deep-scan findings (all 5 fixed)

  • Multi-GB layer OOM — file extraction (x save, content viewer) replaced eager loadLayerTars (full map of blobs in memory) with a lazy blobLoader closure. Peak heap is now bounded by one layer blob plus an explicit per-blob size cap (16 GiB) — malformed archives declaring petabyte layers can no longer trigger runaway allocation. (image/extractor.go, image/archive.go)
  • Non-atomic savex save previously used os.WriteFile, leaving truncated files on process kill / OOM / power loss. Now writes via temp-file + rename in the target's directory; the file at the chosen path always holds either the complete pre-write content or the complete new content.
  • TUI freeze on syntax highlighting — Chroma tokenisation moved off the bubbletea input goroutine via a tea.Cmd. The plain-text view appears immediately; the coloured version swaps in when ready, gated by requestID so stale highlights from interrupted opens are discarded.
  • CJK/emoji column overflow in Waste paneltruncateLeft now measures display columns via lipgloss.Width rather than rune count.
  • Layer blob size cap — explicit MaxLayerBlobSize (16 GiB) ceiling on each layer body read, preventing runaway allocation from a hostile or malformed manifest.

Pre-merge review gate findings (3 CONFIRMED + 2 PLAUSIBLE-promoted, all fixed)

The code-review skill (5 finder angles + 1-vote verifier + sweep) was run on the working-tree diff before commit. Five blocking findings:

  • atomicWriteFile bypassed umasktmp.Chmod(perm) applied literal mode bits. New build-tagged applyUmask helper round-trips through syscall.Umask on POSIX and is a no-op on Windows, restoring os.WriteFile semantics.
  • atomicWriteFile destroyed symlinksos.Rename replaced the link with a regular file. Now filepath.EvalSymlinks resolves first and the rename targets the resolved path, so saving over a symlink updates the link's target rather than the link itself.
  • EvalSymlinks fallback masked all errors — narrowed to os.IsNotExist only. Permission-denied / loop-detection errors now surface to the caller rather than silently writing at the unresolved path.
  • No parent-dir fsync — CHANGELOG and docstring promised power-loss safety; without fsync(parentDir) after rename, ext4 default mount options can lose the rename. Added build-tagged syncDir (POSIX Sync on the directory fd, Windows no-op since NTFS journals MFT updates).
  • Test fd leakTestReadSingleBlobFromSpool_OversizedHeaderRejected never closed its temp *os.File; on Windows this blocked t.TempDir cleanup. Added defer tmp.Close().

Triage of remaining PLAUSIBLE findings (accepted with rationale)

  • syscall.Umask is process-global; concurrent file creation in this codebase only opens 0o600 files, which are unaffected by the round-trip.
  • Symlink TOCTOU window is inherited from any atomic-rename tool — pre-fix os.WriteFile's O_TRUNC follow had the same class of risk.
  • io.ReadAll cap allows a 16 GiB allocation by design (per MaxLayerBlobSize doc: "permissive enough for production ML images").
  • Ownership/ACL not preserved across rewrite — inherent to atomic-replace.
  • scanBlobIndex duplicate-name overwrite — adversarial-only, malformed archive corner.

REFUTED: claimed O(N²) re-scan in walkBackForFiletar.Reader.Next() uses Seek when the underlying reader is io.Seeker (*os.File qualifies), so per-call header scans are header-only seeks, not body re-reads.

Test plan

  • go build ./... clean (Windows dev machine)
  • go vet ./... clean
  • gopls check clean across touched files
  • Cross-builds clean: GOOS=linux, GOOS=darwin, GOOS=windows
  • CI: go test ./... passes on Linux runner
  • Gate C: x save against nginx:latest; verify file contents match
  • Gate C: x save over a symlink; verify the symlink target is updated, link preserved
  • Gate C: open a large Go file in the viewer; verify plain text appears immediately, highlights swap in
  • Gate C: open the Waste panel on an image with CJK paths; verify column alignment

Five user-visible defects from the prior deep scan:

- File extraction (`x` save, content viewer) on multi-GB images no
  longer materialises every layer blob in memory at once. The lazy
  blob loader is bounded by one blob (the one currently being scanned)
  and includes an explicit per-blob size cap so a malformed archive
  declaring a petabyte layer cannot trigger runaway allocation.
- `x` save now writes via temp-file + rename rather than truncating in
  place. A process kill, OOM, or power loss mid-write cannot leave a
  truncated file at the user's chosen path; the target either holds
  the complete pre-write content or the complete new content. The
  save resolves symlinks before writing, applies the process umask,
  and fsyncs the parent directory on POSIX so the rename survives
  power loss.
- Opening a large source file in the viewer no longer freezes the
  TUI while Chroma tokenises. The highlight pass moved off the input
  goroutine; the file shows immediately in plain text and the
  coloured version swaps in when ready.
- Filenames with CJK or wide-emoji characters no longer overflow the
  Waste panel's path column; truncation measures display columns
  rather than rune count.

Pre-merge review (5 finder angles + verifier + sweep, per CLAUDE.md
PR review discipline rule #1) surfaced three additional CONFIRMED
defects introduced by the fixes themselves — all addressed:

- atomicWriteFile bypassed the process umask (tmp.Chmod set the
  literal bits). Now applies umask via a build-tagged helper so the
  final mode matches the prior os.WriteFile semantics on POSIX.
- atomicWriteFile destroyed symlinks (rename replaced the link with
  a regular file). Now resolves the link first and renames onto the
  resolved target, so saving over a symlink updates the link's target
  rather than the link itself.
- TestReadSingleBlobFromSpool_OversizedHeaderRejected leaked a file
  descriptor and on Windows blocked t.TempDir cleanup; added
  defer tmp.Close().
@deveshctl
deveshctl merged commit 2c2a3af into main May 28, 2026
7 checks passed
@deveshctl
deveshctl deleted the fix/deep-scan-2026-05 branch May 28, 2026 18:37
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.

1 participant