fix(brainfs): refuse an in-brain file swapped in behind a symlink; in-brain test targets (#157)
CI / Lint / Test / Vet (pull_request) Successful in 54s
CI / Mirror to GitHub (pull_request) Skipped

Review round 1 on PR #168:

- ReadRegular/ReadRegularMax Lstat the path, then open it again by path;
  os.Root follows an in-root final symlink even with O_NOFOLLOW, so a
  symlink to another brain file swapped in between was read (8k-109k
  raw/ leaks in RebuildIndex, LoadInventory, IndexDoc). The opened handle
  must now be the same file as the Lstat (os.SameFile). This covers every
  ReadRegular caller, slices 1-3 included.
- Every symlink test pointed outside the brain, which os.Root refuses on
  its own. Each now has an in-brain case too: a static in-brain link per
  reader, AssertNoLeakFrom with an in-brain target, and brainfs unit tests
  for ReadDirRegular and the in-brain swap.
- brainfstest: swap links are relative (os.Root refuses every absolute
  link, so an absolute in-brain target tested nothing), and each swapper
  has its own temp name (two swappers on one path deadlocked on each
  other's FIFO).
- IndexDoc indexes only a clean wiki/ or knowledge/ path (callers pass
  caller-supplied paths).
- Rule 9: TestLoadInventoryReturnsErrNotRegularForASymlinkedTypeDir now
  asserts the error; the backfill test says it aborts on a symlinked root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 22:34:46 +02:00
co-authored by Claude Opus 5.5
parent c7a6429bae
commit d90ce66fb2
6 changed files with 160 additions and 11 deletions
@@ -16,6 +16,8 @@ import (
"fmt"
"io/fs"
"path"
"path/filepath"
"strings"
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
"github.com/mathiasbq/hyperguild/ingestion/internal/graph"
@@ -48,6 +50,12 @@ func IndexDoc(ctx context.Context, store Store, brainDir, relPath string) error
if relPath == "" {
return nil
}
// Callers pass caller-supplied paths (brain_tunnel, brain_update): index
// only a clean path under wiki/ or knowledge/.
if path.Clean(relPath) != relPath || !filepath.IsLocal(relPath) ||
(!strings.HasPrefix(relPath, "wiki/") && !strings.HasPrefix(relPath, "knowledge/")) {
return fmt.Errorf("index %q: not a clean wiki/ or knowledge/ path", relPath)
}
bfs, err := brainfs.Open(brainDir)
if err != nil {
return fmt.Errorf("open brain: %w", err)