fix(brain): context, brain_get and brain_read read through brainfs (#157, slice 2)
CI / Lint / Test / Vet (pull_request) Successful in 44s
CI / Mirror to GitHub (pull_request) Skipped

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:
2026-10-04 00:22:47 +02:00
co-authored by Claude Opus 5.5
parent eb77305580
commit b6f7dac46e
9 changed files with 268 additions and 11 deletions
+11 -5
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
"os"
"path/filepath"
"strings"
@@ -71,14 +72,19 @@ func ReadScoped(brainDir, path string, scope Scope) ([]byte, error) {
}
func readContained(t Target) ([]byte, error) {
fi, err := os.Stat(t.Abs)
// Through the brain root, regular files only, size checked on the opened
// handle (hyperguild#157): Resolve's Lstat could be raced by a swap, and
// os.ReadFile blocked on a FIFO.
brainDir := strings.TrimSuffix(t.Abs, string(filepath.Separator)+filepath.FromSlash(t.Rel))
bfs, err := brainfs.Open(brainDir)
if err != nil {
return nil, fmt.Errorf("notes: stat %s: %w", t.Rel, err)
return nil, fmt.Errorf("notes: %w", err)
}
if fi.Size() > maxNoteBytes {
return nil, fmt.Errorf("%w: %s is %d bytes (limit %d)", ErrTooLarge, t.Rel, fi.Size(), maxNoteBytes)
defer func() { _ = bfs.Close() }()
b, err := bfs.ReadRegularMax(t.Rel, maxNoteBytes)
if errors.Is(err, brainfs.ErrTooLarge) {
return nil, fmt.Errorf("%w: %s (limit %d)", ErrTooLarge, t.Rel, maxNoteBytes)
}
b, err := os.ReadFile(t.Abs)
if err != nil {
return nil, fmt.Errorf("notes: read %s: %w", t.Rel, err)
}