The tripwire looked only for symlinks. A FIFO in the brain passed it: at classification.yaml it made startup exit (classification.Load refuses a non-regular file, #176), and at a note path it could hang a reader that skips the handle check. - Scan reports special files (anything not a regular file, directory or symlink) in Result.Special; Clean() and String() include them, so the startup check refuses with a clear message naming the path. - Watch removes them as it removes symlinks, through an os.Root, after re-checking each is still special (a regular file, directory or symlink swapped in is left alone and reported). The live brain has no special files (checked before this change), so the startup check does not newly refuse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.7 KiB
Go
75 lines
2.7 KiB
Go
package symlinkguard
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// hyperguild#179: the tripwire looked only for symlinks, so a FIFO (or
|
|
// socket) in the brain passed it. A FIFO at classification.yaml then made
|
|
// startup exit; at a note path it could hang readers that skip the handle
|
|
// check.
|
|
func TestScanReportsFIFOsAndSocketsAsSpecialFiles(t *testing.T) {
|
|
brain := t.TempDir()
|
|
require.NoError(t, os.MkdirAll(filepath.Join(brain, "wiki", "w"), 0o755))
|
|
require.NoError(t, syscall.Mkfifo(filepath.Join(brain, "classification.yaml"), 0o644))
|
|
require.NoError(t, syscall.Mkfifo(filepath.Join(brain, "wiki", "w", "f.md"), 0o644))
|
|
l, err := net.Listen("unix", filepath.Join(brain, "s.sock"))
|
|
require.NoError(t, err)
|
|
defer func() { _ = l.Close() }()
|
|
require.NoError(t, os.WriteFile(filepath.Join(brain, "wiki", "w", "n.md"), []byte("n"), 0o644))
|
|
|
|
r, err := Scan(brain)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"classification.yaml", "s.sock", "wiki/w/f.md"}, r.Special)
|
|
assert.False(t, r.Clean())
|
|
assert.Contains(t, r.String(), "special file")
|
|
}
|
|
|
|
// Watch removes special files as it removes symlinks, and leaves regular
|
|
// files alone.
|
|
func TestWatchRemovesSpecialFilesButNotRegularFiles(t *testing.T) {
|
|
brain := t.TempDir()
|
|
fifo := filepath.Join(brain, "f.md")
|
|
require.NoError(t, syscall.Mkfifo(fifo, 0o644))
|
|
regular := filepath.Join(brain, "n.md")
|
|
require.NoError(t, os.WriteFile(regular, []byte("n"), 0o644))
|
|
n := &recNotifier{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go Watch(ctx, brain, 10*time.Millisecond, n)
|
|
require.Eventually(t, func() bool {
|
|
_, err := os.Lstat(fifo)
|
|
return os.IsNotExist(err)
|
|
}, 2*time.Second, 10*time.Millisecond, "the FIFO is removed")
|
|
require.Eventually(t, func() bool { return n.count() >= 1 }, 2*time.Second, 10*time.Millisecond)
|
|
b, err := os.ReadFile(regular)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "n", string(b))
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
assert.Contains(t, n.msgs[0], "f.md")
|
|
}
|
|
|
|
// removeSpecial re-checks the type: a path that is now a regular file or a
|
|
// directory is not removed.
|
|
func TestRemoveSpecialRefusesRegularFilesAndDirectories(t *testing.T) {
|
|
brain := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(brain, "n.md"), []byte("n"), 0o644))
|
|
require.NoError(t, os.MkdirAll(filepath.Join(brain, "d"), 0o755))
|
|
require.NoError(t, syscall.Mkfifo(filepath.Join(brain, "f.md"), 0o644))
|
|
removed, failed := removeSpecial(brain, []string{"n.md", "d", "f.md"})
|
|
assert.Equal(t, []string{"f.md"}, removed)
|
|
assert.Equal(t, []string{"n.md", "d"}, failed)
|
|
_, err := os.Stat(filepath.Join(brain, "n.md"))
|
|
assert.NoError(t, err)
|
|
}
|