Review round 1 on PR #170 found two more handlers with the #169 class of bug (verified by execution): - brain_update resolved its caller path with resolveWithin, a lexical containment check: any brain file could be superseded (classification.yaml, raw/, sessions/), and a final symlink was written through. UpdateNote now resolves like brain_edit (notes.Resolve, ScopeNotes: knowledge/ and wiki/ .md notes), and reads and rewrites through brainfs (ReadRegular, WriteRegular). - session_log joined the caller's session_id onto the sessions dir and opened it O_CREATE|O_APPEND: an id with path segments created or appended a .jsonl file anywhere the pod could write. The id must now be a plain name (^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$, which every live session file matches), and the open adds O_NOFOLLOW|O_NONBLOCK. Read checks the id too. - Tunnel tests: the .txt case now names an existing file (it failed on "missing" before), the symlink test adds an in-brain target, and the FIFO comment no longer claims the handle check is the only guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
5.5 KiB
Go
142 lines
5.5 KiB
Go
package brain_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/brain"
|
|
)
|
|
|
|
// brain_tunnel passes caller-supplied source and target paths to WriteTunnel,
|
|
// which checked only "wiki/<wing>/..." and then joined them onto the brain
|
|
// dir: ".." segments resolved outside the brain and appendSeeAlso rewrote the
|
|
// file there.
|
|
func TestWriteTunnelRefusesATargetThatTraversesOutOfTheBrain(t *testing.T) {
|
|
brain0 := t.TempDir()
|
|
src := filepath.Join(brain0, "wiki", "a", "facts", "s.md")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(src), 0o755))
|
|
require.NoError(t, os.WriteFile(src, []byte("---\ntitle: S\n---\nbody\n"), 0o644))
|
|
victim := filepath.Join(t.TempDir(), "victim.md")
|
|
require.NoError(t, os.WriteFile(victim, []byte("untouched\n"), 0o644))
|
|
|
|
up := strings.Repeat("../", strings.Count(filepath.Clean(brain0), "/")+3)
|
|
target := "wiki/b/x/" + up + strings.TrimPrefix(victim, "/")
|
|
err := brain.WriteTunnel(brain0, "wiki/a/facts/s.md", target)
|
|
assert.Error(t, err)
|
|
got, rerr := os.ReadFile(victim)
|
|
require.NoError(t, rerr)
|
|
assert.Equal(t, "untouched\n", string(got), "a file outside the brain was modified")
|
|
}
|
|
|
|
func TestWriteTunnelRefusesUncleanOrNonWikiPaths(t *testing.T) {
|
|
brain0 := t.TempDir()
|
|
for _, p := range []string{"wiki/a/facts/s.md", "wiki/b/facts/t.md", "wiki/b/facts/t.txt", "raw/r.md"} {
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(brain0, p)), 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(brain0, p), []byte("x\n"), 0o644))
|
|
}
|
|
for _, target := range []string{
|
|
"wiki/b/facts/../../../raw/r.md",
|
|
"wiki/b/./facts/t.md",
|
|
"wiki/b//facts/t.md",
|
|
"/wiki/b/facts/t.md",
|
|
"wiki/b/facts/t.txt",
|
|
"wiki/b/facts/",
|
|
} {
|
|
assert.Error(t, brain.WriteTunnel(brain0, "wiki/a/facts/s.md", target), target)
|
|
}
|
|
for _, p := range []string{"raw/r.md", "wiki/b/facts/t.txt"} {
|
|
got, err := os.ReadFile(filepath.Join(brain0, p))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "x\n", string(got), p)
|
|
}
|
|
require.NoError(t, brain.WriteTunnel(brain0, "wiki/a/facts/s.md", "wiki/b/facts/t.md"), "positive control")
|
|
got, err := os.ReadFile(filepath.Join(brain0, "wiki", "b", "facts", "t.md"))
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(got), "[[a/facts/s]]")
|
|
}
|
|
|
|
func TestWriteTunnelDoesNotWriteThroughASymlinkedNote(t *testing.T) {
|
|
brain0 := t.TempDir()
|
|
src := filepath.Join(brain0, "wiki", "a", "facts", "s.md")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(src), 0o755))
|
|
require.NoError(t, os.WriteFile(src, []byte("x\n"), 0o644))
|
|
tdir := filepath.Join(brain0, "wiki", "b", "facts")
|
|
require.NoError(t, os.MkdirAll(tdir, 0o755))
|
|
victim := filepath.Join(t.TempDir(), "victim.md")
|
|
require.NoError(t, os.WriteFile(victim, []byte("untouched\n"), 0o644))
|
|
require.NoError(t, os.Symlink(victim, filepath.Join(tdir, "t.md")))
|
|
assert.Error(t, brain.WriteTunnel(brain0, "wiki/a/facts/s.md", "wiki/b/facts/t.md"))
|
|
got, err := os.ReadFile(victim)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "untouched\n", string(got))
|
|
|
|
// The same with a link to a file elsewhere in the brain, which os.Root
|
|
// would follow.
|
|
inVictim := filepath.Join(brain0, "raw", "v.md")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(inVictim), 0o755))
|
|
require.NoError(t, os.WriteFile(inVictim, []byte("untouched\n"), 0o644))
|
|
require.NoError(t, os.Symlink(filepath.Join("..", "..", "..", "raw", "v.md"), filepath.Join(tdir, "u.md")))
|
|
assert.Error(t, brain.WriteTunnel(brain0, "wiki/a/facts/s.md", "wiki/b/facts/u.md"))
|
|
got, err = os.ReadFile(inVictim)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "untouched\n", string(got))
|
|
got, err = os.ReadFile(src)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "x\n", string(got), "the source is not touched when the target is refused")
|
|
}
|
|
|
|
// The target is swapped between a regular note and a symlink to an in-brain
|
|
// file outside wiki/ while WriteTunnel runs: the read and the write each
|
|
// refuse a symlink, so the other file is never rewritten.
|
|
func TestWriteTunnelNeverRewritesASymlinkSwappedInForTheTarget(t *testing.T) {
|
|
brain0 := t.TempDir()
|
|
src := filepath.Join(brain0, "wiki", "a", "facts", "s.md")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(src), 0o755))
|
|
require.NoError(t, os.WriteFile(src, []byte("x\n"), 0o644))
|
|
tdir := filepath.Join(brain0, "wiki", "b", "facts")
|
|
require.NoError(t, os.MkdirAll(tdir, 0o755))
|
|
victim := filepath.Join(brain0, "raw", "victim.md")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(victim), 0o755))
|
|
require.NoError(t, os.WriteFile(victim, []byte("untouched\n"), 0o644))
|
|
tgt := filepath.Join(tdir, "t.md")
|
|
require.NoError(t, os.WriteFile(tgt, []byte("t\n"), 0o644))
|
|
|
|
var stop atomic.Bool
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
tmp := tgt + ".swap"
|
|
for i := 0; !stop.Load(); i++ {
|
|
_ = os.Remove(tmp)
|
|
if i%2 == 0 {
|
|
_ = os.WriteFile(tmp, []byte("t\n"), 0o644)
|
|
} else {
|
|
_ = os.Symlink(filepath.Join("..", "..", "..", "raw", "victim.md"), tmp)
|
|
}
|
|
_ = os.Rename(tmp, tgt)
|
|
}
|
|
}()
|
|
ok := 0
|
|
for deadline := time.Now().Add(1500 * time.Millisecond); time.Now().Before(deadline); {
|
|
if brain.WriteTunnel(brain0, "wiki/a/facts/s.md", "wiki/b/facts/t.md") == nil {
|
|
ok++
|
|
}
|
|
_ = os.WriteFile(src, []byte("x\n"), 0o644)
|
|
}
|
|
stop.Store(true)
|
|
wg.Wait()
|
|
got, err := os.ReadFile(victim)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "untouched\n", string(got), "the swapped-in symlink's target was rewritten")
|
|
assert.Positive(t, ok, "positive control: some tunnels were written to the regular target")
|
|
}
|