fix(brainfs): refuse a file behind a symlinked directory for reads and writes (#173)
CI / Lint / Test / Vet (pull_request) Successful in 55s
CI / Mirror to GitHub (pull_request) Skipped

os.Root follows an intermediate directory symlink that stays inside the
brain. A dir symlink inside wiki/ pointing elsewhere in the brain let
brain_update and brain_tunnel rewrite an existing .md there (e.g. under
raw/), and session_log followed a symlinked sessions dir.

- WriteRegular, ReadRegular and ReadRegularMax require every directory
  above the file to be a real directory (realDir). Reads too: brain_tunnel
  read the redirected target, then rewrote the source before the target's
  write was refused; and brain_read/brain_get could read another part of
  the brain through such a path.
- session.Append refuses a sessions dir that is not a real directory.
- A directory swapped in after the check remains the #167 residual.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 20:54:00 +02:00
co-authored by Claude Opus 5.5
parent febab3e45d
commit 87dace37b1
6 changed files with 121 additions and 2 deletions
+7
View File
@@ -68,6 +68,13 @@ func Append(sessionsDir, sessionID string, entry Entry) error {
if err := os.MkdirAll(sessionsDir, 0o755); err != nil {
return fmt.Errorf("create sessions dir: %w", err)
}
// The sessions dir itself must be a real directory, not a symlink that
// redirects the log elsewhere (hyperguild#173).
if fi, err := os.Lstat(sessionsDir); err != nil {
return fmt.Errorf("stat sessions dir: %w", err)
} else if !fi.IsDir() {
return fmt.Errorf("sessions dir %s is not a real directory", sessionsDir)
}
path := filepath.Join(sessionsDir, sessionID+".jsonl")
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0o644)
if err != nil {