fix(brainfs): refuse an in-brain file swapped in behind a symlink; in-brain test targets (#157)
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:
@@ -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() })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user