fix(brainfs,session): clean paths only; session logs open through the brain root (#173)
CI / Lint / Test / Vet (pull_request) Successful in 1m1s
CI / Mirror to GitHub (pull_request) Skipped

Review round 1 on PR #187:

- realDir checked the cleaned parent while Lstat/OpenFile used the raw
  path; os.Root resolves ".." after following a symlink, so a ".." placed
  after a symlinked directory passed the check and redirected the read or
  write. ReadRegular, ReadRegularMax and WriteRegular now refuse a path
  that is not clean and local (ErrNotRegular). No current entry point
  reached it (callers clean first); the primitive no longer relies on that.
- session.Append checked the sessions dir with Lstat, then opened by plain
  path: a symlink swapped in between created logs outside the brain
  (29k in the reviewer's RENAME_EXCHANGE race). It now opens through an
  os.Root on the sessions dir's parent, so a swap can only redirect inside
  the brain (#167), and, since os.Root follows an in-root final symlink
  even with O_NOFOLLOW, writes only after confirming the path is the
  regular file it opened (Lstat + os.SameFile).
- Endpoint tests assert ErrorIs(brainfs.ErrNotRegular) and have positive
  controls.

golang.org/x/sys moves from indirect to direct (unix.Renameat2 in the
session race test); it was already in the module graph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 21:08:47 +02:00
co-authored by Claude Opus 5.5
parent 87dace37b1
commit 9a288bea6f
7 changed files with 161 additions and 10 deletions
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/mathiasbq/hyperguild/ingestion/internal/api"
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
)
// hyperguild#173: a directory symlink inside wiki/ that points elsewhere in
@@ -23,7 +24,13 @@ func TestUpdateNoteRefusesANoteBehindASymlinkedDirectory(t *testing.T) {
require.NoError(t, os.Symlink(filepath.Join("..", "..", "raw", "facts"), filepath.Join(brain, "wiki", "w", "alias")))
_, _, _, err := api.UpdateNote(brain, api.UpdateNoteOptions{Path: "wiki/w/alias/v.md", Content: "replaced"})
assert.Error(t, err)
assert.ErrorIs(t, err, brainfs.ErrNotRegular)
got, _ := os.ReadFile(victim)
assert.Contains(t, string(got), "untouched")
real := filepath.Join(brain, "wiki", "w", "facts", "r.md")
require.NoError(t, os.MkdirAll(filepath.Dir(real), 0o755))
require.NoError(t, os.WriteFile(real, []byte("---\ntitle: r\n---\nold\n"), 0o644))
_, _, _, err = api.UpdateNote(brain, api.UpdateNoteOptions{Path: "wiki/w/facts/r.md", Content: "new"})
require.NoError(t, err, "positive control: a note in a real directory is updated")
}