fix(brainfs,session): clean paths only; session logs open through the brain root (#173)
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:
@@ -65,21 +65,42 @@ func Append(sessionsDir, sessionID string, entry Entry) error {
|
||||
if err := checkID(sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(sessionsDir, 0o755); err != nil {
|
||||
// Opened through an os.Root on the sessions dir's parent (the brain dir),
|
||||
// so a sessions dir swapped for a symlink after the check below can
|
||||
// redirect the log only inside that parent, never outside it (#173; the
|
||||
// in-brain swap is the #167 residual).
|
||||
parentDir, name := filepath.Dir(sessionsDir), filepath.Base(sessionsDir)
|
||||
if err := os.MkdirAll(parentDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create sessions parent: %w", err)
|
||||
}
|
||||
root, err := os.OpenRoot(parentDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open sessions parent: %w", err)
|
||||
}
|
||||
defer func() { _ = root.Close() }()
|
||||
if err := root.Mkdir(name, 0o755); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
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 {
|
||||
if fi, err := root.Lstat(name); 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)
|
||||
logRel := name + "/" + sessionID + ".jsonl"
|
||||
f, err := root.OpenFile(logRel, os.O_CREATE|os.O_APPEND|os.O_WRONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open session log: %w", err)
|
||||
}
|
||||
// os.Root follows an in-root final symlink even with O_NOFOLLOW, so
|
||||
// confirm, before writing, that the path is the regular file we opened.
|
||||
hfi, herr := f.Stat()
|
||||
lfi, lerr := root.Lstat(logRel)
|
||||
if herr != nil || lerr != nil || !lfi.Mode().IsRegular() || !os.SameFile(lfi, hfi) {
|
||||
_ = f.Close()
|
||||
return fmt.Errorf("session log %s is not a regular file", logRel)
|
||||
}
|
||||
|
||||
line, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user