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:
@@ -351,3 +351,28 @@ func TestReadAndWriteRegularRefuseAFileBehindASymlinkedDirectory(t *testing.T) {
|
||||
require.NoError(t, err, "positive control: the real path still reads")
|
||||
assert.Equal(t, "untouched", string(got))
|
||||
}
|
||||
|
||||
// realDir checks the cleaned parent, so the I/O must use that same clean
|
||||
// path: os.Root resolves ".." after following a symlink, and a ".." placed
|
||||
// after a symlinked directory would otherwise pass the check and redirect.
|
||||
func TestReadAndWriteRegularRefuseAPathThatIsNotClean(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw", "facts"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "raw", "v.md"), []byte("untouched"), 0o644))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki"), 0o755))
|
||||
require.NoError(t, os.Symlink(filepath.Join("..", "raw", "facts"), filepath.Join(dir, "wiki", "alias")))
|
||||
b, err := Open(dir)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = b.Close() }()
|
||||
|
||||
for _, rel := range []string{"wiki/alias/../v.md", "./raw/v.md", "raw//v.md", "/raw/v.md"} {
|
||||
got, err := b.ReadRegular(rel)
|
||||
assert.ErrorIs(t, err, ErrNotRegular, rel)
|
||||
assert.NotContains(t, string(got), "untouched", rel)
|
||||
_, err = b.ReadRegularMax(rel, 100)
|
||||
assert.ErrorIs(t, err, ErrNotRegular, rel)
|
||||
assert.ErrorIs(t, b.WriteRegular(rel, []byte("OVERWRITTEN")), ErrNotRegular, rel)
|
||||
}
|
||||
got, _ := os.ReadFile(filepath.Join(dir, "raw", "v.md"))
|
||||
assert.Equal(t, "untouched", string(got))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user