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
+10 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs/brainfstest"
)
@@ -23,6 +24,8 @@ func TestLoadInventoryRefusesSymlinksAndSurvivesSwapRaces(t *testing.T) {
require.NoError(t, os.MkdirAll(dir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "ok.md"), []byte("---\ntitle: zqinside\n---\n"), 0o644))
require.NoError(t, os.Symlink(brainfstest.Outside(t), filepath.Join(dir, "leak.md")))
inBrain := brainfstest.WriteSentinelAt(t, filepath.Join(brain, "raw", "s.md"))
require.NoError(t, os.Symlink(filepath.Join("..", "..", "raw", "s.md"), filepath.Join(dir, "inlink.md")))
inv, err := LoadInventory(brain)
require.NoError(t, err)
@@ -30,6 +33,7 @@ func TestLoadInventoryRefusesSymlinksAndSurvivesSwapRaces(t *testing.T) {
assert.Contains(t, got, "zqinside", "positive control")
assert.NotContains(t, got, brainfstest.Sentinel)
assert.NotContains(t, got, "leak", "a symlinked page is not listed")
assert.NotContains(t, got, "inlink", "a page linked to another brain file is not listed")
p := filepath.Join(dir, "r.md")
read := func() string {
@@ -37,16 +41,18 @@ func TestLoadInventoryRefusesSymlinksAndSurvivesSwapRaces(t *testing.T) {
return fmt.Sprintf("%+v", inv)
}
brainfstest.AssertNoLeak(t, p, "---\ntitle: zqinside\n---\n", 1500*time.Millisecond, read)
brainfstest.AssertNoLeakFrom(t, p, "---\ntitle: zqinside\n---\n", inBrain, 1500*time.Millisecond, read)
brainfstest.AssertNoHang(t, p, "---\ntitle: zqinside\n---\n", 1500*time.Millisecond, 6*time.Second, func() { _ = read() })
}
func TestLoadInventorySkipsASymlinkedTypeDir(t *testing.T) {
func TestLoadInventoryReturnsErrNotRegularForASymlinkedTypeDir(t *testing.T) {
brain := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(brain, "wiki"), 0o755))
require.NoError(t, os.MkdirAll(filepath.Join(brain, "raw"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(brain, "raw", "r.md"), []byte("---\ntitle: zqrawonly\n---\n"), 0o644))
require.NoError(t, os.Symlink(filepath.Join("..", "raw"), filepath.Join(brain, "wiki", "concepts")))
inv, _ := LoadInventory(brain)
inv, err := LoadInventory(brain)
require.ErrorIs(t, err, brainfs.ErrNotRegular)
assert.NotContains(t, fmt.Sprintf("%+v", inv), "zqrawonly")
}
@@ -67,5 +73,7 @@ func TestRebuildIndexDoesNotReadAPageSwappedForASymlink(t *testing.T) {
return string(b)
}
brainfstest.AssertNoLeak(t, p, "---\ntitle: R\n---\nzqinside sentence.\n", 1500*time.Millisecond, read)
inBrain := brainfstest.WriteSentinelAt(t, filepath.Join(brain, "raw", "s.md"))
brainfstest.AssertNoLeakFrom(t, p, "---\ntitle: R\n---\nzqinside sentence.\n", inBrain, 1500*time.Millisecond, read)
brainfstest.AssertNoHang(t, p, "---\ntitle: R\n---\nzqinside sentence.\n", 1500*time.Millisecond, 6*time.Second, func() { _ = read() })
}