fix(brain): context, brain_get and brain_read read through brainfs (#157, slice 2)
Option 1 of #157, slice 2. readExcerpt (brain_context), api.ReadNote (brain_get / read-after-write) and notes.ReadScoped (brain_read) read the Syncthing-shared brain by path. Under a symlink swap race each leaked an outside file hundreds of thousands of times in 1.5s, including brain_read, whose Lstat check was raced by the reopen-by-name; a FIFO blocked os.ReadFile. - All three read through brainfs (os.Root, regular files only, non-blocking open, type check on the handle). - brainfs.ReadRegularMax checks a size limit on the opened handle; notes keeps its ErrTooLarge. - New internal/brainfs/brainfstest: the shared swap-race harness (symlink and held-writer FIFO swappers, awaited before TempDir cleanup; AssertNoLeak / AssertNoHang) for this and later slices. Tests red first: each reader under a symlink-swap race and a held-writer FIFO race, plus static checks. Flipped: readExcerpt back to os.ReadFile is red. Refs #157 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs/brainfstest"
|
||||
)
|
||||
|
||||
// hyperguild#157 slice 2: readExcerpt (brain_context) read by path, so a
|
||||
// symlink in the Syncthing-shared brain put an outside file in context, and
|
||||
// a FIFO hung it. It now reads through brainfs.
|
||||
func TestReadExcerptIsConfinedToRegularFilesInTheBrain(t *testing.T) {
|
||||
brain := t.TempDir()
|
||||
facts := filepath.Join(brain, "wiki", "w", "facts")
|
||||
require.NoError(t, os.MkdirAll(facts, 0o755))
|
||||
require.NoError(t, os.Symlink(brainfstest.Outside(t), filepath.Join(facts, "leak.md")))
|
||||
assert.NotContains(t, readExcerpt(brain, "wiki/w/facts/leak.md", 500), brainfstest.Sentinel)
|
||||
|
||||
p := filepath.Join(facts, "r.md")
|
||||
require.NoError(t, os.WriteFile(p, []byte("---\nx: y\n---\ninside"), 0o644))
|
||||
assert.Equal(t, "inside", readExcerpt(brain, "wiki/w/facts/r.md", 500))
|
||||
brainfstest.AssertNoLeak(t, p, "---\nx: y\n---\ninside", 1500*time.Millisecond, func() string {
|
||||
return readExcerpt(brain, "wiki/w/facts/r.md", 500)
|
||||
})
|
||||
brainfstest.AssertNoHang(t, p, "---\nx: y\n---\ninside", 1500*time.Millisecond, 6*time.Second, func() {
|
||||
_ = readExcerpt(brain, "wiki/w/facts/r.md", 500)
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -181,8 +181,13 @@ func readExcerpt(brainDir, relPath string, maxLen int) string {
|
||||
if relPath == "" {
|
||||
return ""
|
||||
}
|
||||
full := filepath.Join(brainDir, filepath.FromSlash(relPath))
|
||||
content, err := os.ReadFile(full)
|
||||
// Through the brain root, regular files only (hyperguild#157).
|
||||
bfs, err := brainfs.Open(brainDir)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer func() { _ = bfs.Close() }()
|
||||
content, err := bfs.ReadRegular(filepath.ToSlash(relPath))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user