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>
93 lines
4.2 KiB
Go
93 lines
4.2 KiB
Go
package graphsync
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs/brainfstest"
|
|
)
|
|
|
|
func (f *fakeStore) dump() string {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return fmt.Sprintf("%+v %+v", f.upserts, f.replaces)
|
|
}
|
|
|
|
// hyperguild#157 slice 4: IndexDoc and BackfillFromBrainDir read notes with
|
|
// os.ReadFile, so a symlinked note put the target's title and wikilinks into
|
|
// the graph, and a FIFO hung the read.
|
|
func TestIndexDocRefusesSymlinksAndSurvivesSwapRaces(t *testing.T) {
|
|
brain := t.TempDir()
|
|
facts := filepath.Join(brain, "wiki", "w", "facts")
|
|
require.NoError(t, os.MkdirAll(facts, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(facts, "ok.md"), []byte("---\ntitle: zqinside\n---\nbody [[other]]\n"), 0o644))
|
|
require.NoError(t, os.Symlink(brainfstest.Outside(t), filepath.Join(facts, "leak.md")))
|
|
|
|
store := newFakeStore()
|
|
require.NoError(t, IndexDoc(context.Background(), store, brain, "wiki/w/facts/ok.md"))
|
|
assert.Contains(t, store.dump(), "zqinside", "positive control: a regular note is indexed")
|
|
assert.Error(t, IndexDoc(context.Background(), store, brain, "wiki/w/facts/leak.md"))
|
|
// A link to a file elsewhere in the brain: os.Root allows it, only the
|
|
// regular-file check refuses it.
|
|
inBrain := brainfstest.WriteSentinelAt(t, filepath.Join(brain, "raw", "s.md"))
|
|
require.NoError(t, os.Symlink(filepath.Join("..", "..", "..", "raw", "s.md"), filepath.Join(facts, "inlink.md")))
|
|
assert.Error(t, IndexDoc(context.Background(), store, brain, "wiki/w/facts/inlink.md"))
|
|
assert.NotContains(t, store.dump(), brainfstest.Sentinel)
|
|
|
|
p := filepath.Join(facts, "r.md")
|
|
read := func() string {
|
|
s := newFakeStore()
|
|
_ = IndexDoc(context.Background(), s, brain, "wiki/w/facts/r.md")
|
|
return s.dump()
|
|
}
|
|
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 TestBackfillSkipsSymlinkedNotesAndAbortsOnASymlinkedRoot(t *testing.T) {
|
|
brain := t.TempDir()
|
|
facts := filepath.Join(brain, "wiki", "w", "facts")
|
|
require.NoError(t, os.MkdirAll(facts, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(facts, "ok.md"), []byte("---\ntitle: zqinside\n---\n"), 0o644))
|
|
require.NoError(t, os.Symlink(brainfstest.Outside(t), filepath.Join(facts, "leak.md")))
|
|
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", "r.md"), filepath.Join(facts, "inlink.md")))
|
|
require.NoError(t, os.Symlink(".", filepath.Join(brain, "knowledge")))
|
|
|
|
store := newFakeStore()
|
|
_, err := BackfillFromBrainDir(context.Background(), store, brain)
|
|
assert.Error(t, err, "a symlinked root aborts the backfill")
|
|
assert.NotContains(t, store.dump(), brainfstest.Sentinel)
|
|
assert.NotContains(t, store.dump(), "zqrawonly")
|
|
|
|
require.NoError(t, os.Remove(filepath.Join(brain, "knowledge")))
|
|
store = newFakeStore()
|
|
n, err := BackfillFromBrainDir(context.Background(), store, brain)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, n, "only the regular note is indexed")
|
|
assert.Contains(t, store.dump(), "zqinside")
|
|
assert.NotContains(t, store.dump(), brainfstest.Sentinel)
|
|
}
|
|
|
|
// IndexDoc gets caller-supplied paths (brain_tunnel, brain_update): only a
|
|
// clean path under wiki/ or knowledge/ is indexed.
|
|
func TestIndexDocRefusesPathsOutsideWikiAndKnowledgeAndUncleanPaths(t *testing.T) {
|
|
brain := t.TempDir()
|
|
brainfstest.WriteSentinelAt(t, filepath.Join(brain, "raw", "secret.md"))
|
|
require.NoError(t, os.MkdirAll(filepath.Join(brain, "wiki", "w"), 0o755))
|
|
for _, p := range []string{"raw/secret.md", "wiki/w/../../raw/secret.md", "./raw/secret.md", "/raw/secret.md"} {
|
|
store := newFakeStore()
|
|
assert.Error(t, IndexDoc(context.Background(), store, brain, p), p)
|
|
assert.NotContains(t, store.dump(), brainfstest.Sentinel, p)
|
|
}
|
|
}
|