fix(brainfs): refuse an in-brain file swapped in behind a symlink; in-brain test targets (#157)
Review round 1 on PR #168: - ReadRegular/ReadRegularMax Lstat the path, then open it again by path; os.Root follows an in-root final symlink even with O_NOFOLLOW, so a symlink to another brain file swapped in between was read (8k-109k raw/ leaks in RebuildIndex, LoadInventory, IndexDoc). The opened handle must now be the same file as the Lstat (os.SameFile). This covers every ReadRegular caller, slices 1-3 included. - Every symlink test pointed outside the brain, which os.Root refuses on its own. Each now has an in-brain case too: a static in-brain link per reader, AssertNoLeakFrom with an in-brain target, and brainfs unit tests for ReadDirRegular and the in-brain swap. - brainfstest: swap links are relative (os.Root refuses every absolute link, so an absolute in-brain target tested nothing), and each swapper has its own temp name (two swappers on one path deadlocked on each other's FIFO). - IndexDoc indexes only a clean wiki/ or knowledge/ path (callers pass caller-supplied paths). - Rule 9: TestLoadInventoryReturnsErrNotRegularForASymlinkedTypeDir now asserts the error; the backfill test says it aborts on a symlinked root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,7 +114,7 @@ func (b *Brain) ReadRegular(rel string) ([]byte, error) {
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular)
|
||||
}
|
||||
return b.readOpened(rel)
|
||||
return b.readOpenedMax(rel, -1, fi)
|
||||
}
|
||||
|
||||
// readOpened opens rel non-blocking and reads it only if the OPENED handle is
|
||||
@@ -128,7 +128,7 @@ func (b *Brain) ReadRegular(rel string) ([]byte, error) {
|
||||
// cannot create one; fs.protected_hardlinks=1 and the pod's secrets are on a
|
||||
// different filesystem from the brain volume, so a local caller cannot link
|
||||
// them in. Refusing Nlink > 1 is the next step if either changes.
|
||||
func (b *Brain) readOpened(rel string) ([]byte, error) { return b.readOpenedMax(rel, -1) }
|
||||
func (b *Brain) readOpened(rel string) ([]byte, error) { return b.readOpenedMax(rel, -1, nil) }
|
||||
|
||||
// ReadRegularMax is ReadRegular with a size limit, checked on the opened
|
||||
// handle before reading (max < 0 means no limit).
|
||||
@@ -140,10 +140,14 @@ func (b *Brain) ReadRegularMax(rel string, max int64) ([]byte, error) {
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular)
|
||||
}
|
||||
return b.readOpenedMax(rel, max)
|
||||
return b.readOpenedMax(rel, max, fi)
|
||||
}
|
||||
|
||||
func (b *Brain) readOpenedMax(rel string, max int64) ([]byte, error) {
|
||||
// readOpenedMax opens rel and reads it if the opened handle is a regular file
|
||||
// and, when lfi is set, the same file the caller Lstat-ed: os.Root follows an
|
||||
// in-root final symlink even with O_NOFOLLOW, so a symlink to another brain
|
||||
// file swapped in between the Lstat and the open is refused only here.
|
||||
func (b *Brain) readOpenedMax(rel string, max int64, lfi fs.FileInfo) ([]byte, error) {
|
||||
f, err := b.root.OpenFile(rel, os.O_RDONLY|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -156,6 +160,9 @@ func (b *Brain) readOpenedMax(rel string, max int64) ([]byte, error) {
|
||||
if !hfi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular)
|
||||
}
|
||||
if lfi != nil && !os.SameFile(lfi, hfi) {
|
||||
return nil, fmt.Errorf("%s: changed while opening: %w", rel, ErrNotRegular)
|
||||
}
|
||||
if max >= 0 && hfi.Size() > max {
|
||||
return nil, fmt.Errorf("%s is %d bytes (limit %d): %w", rel, hfi.Size(), max, ErrTooLarge)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user