package session import ( "errors" "fmt" "os" "path/filepath" "sync/atomic" "syscall" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" ) // session_log passed the caller's session_id straight into // filepath.Join(sessionsDir, id+".jsonl") and opened it O_CREATE|O_APPEND: // path segments in the id created or appended files anywhere the pod could // write. The id is now a plain name, and the open does not follow a final // symlink. func TestAppendRefusesSessionIDsThatAreNotPlainNames(t *testing.T) { root := t.TempDir() dir := filepath.Join(root, "sessions") for _, id := range []string{"../x", "a/b", "/abs", ".", "..", ".hidden", "", "a\\b", "a\x00b"} { assert.Error(t, Append(dir, id, Entry{SessionID: id}), "%q", id) } ents, _ := os.ReadDir(root) for _, e := range ents { assert.Equal(t, "sessions", e.Name(), "nothing created beside the sessions dir") } for _, id := range []string{"1557b2e1-bf0d-5c4f-bbe3-0754d087e356", "_routing", "2026-05-05-infra.plan", "0"} { require.NoError(t, Append(dir, id, Entry{SessionID: id}), "positive control: %q", id) } } func TestAppendDoesNotWriteThroughASymlinkedLog(t *testing.T) { root := t.TempDir() dir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(dir, 0o755)) victim := filepath.Join(root, "victim.md") require.NoError(t, os.WriteFile(victim, []byte("untouched\n"), 0o644)) require.NoError(t, os.Symlink(filepath.Join("..", "victim.md"), filepath.Join(dir, "s.jsonl"))) assert.Error(t, Append(dir, "s", Entry{SessionID: "s"})) got, _ := os.ReadFile(victim) assert.Equal(t, "untouched\n", string(got)) } func TestReadRefusesSessionIDsThatAreNotPlainNames(t *testing.T) { root := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(root, "x.jsonl"), []byte(`{"session_id":"x"}`+"\n"), 0o644)) _, err := Read(filepath.Join(root, "sessions"), "../x") assert.Error(t, err) } // A name that starts plainly and then climbs out with ".." is exactly what the // ban on "/" in the id's tail stops: filepath.Join cleans "a/../../x" to a // path beside the sessions dir without needing sessions/a to exist. The // assertion is on the refusal itself (ErrInvalidID), not on any error. func TestAppendRefusesAnIDThatClimbsOutAfterAPlainPrefix(t *testing.T) { root := t.TempDir() dir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(dir, 0o755)) for _, id := range []string{"a/../../escape", "a/../../../escape", "a/../b"} { err := Append(dir, id, Entry{SessionID: id}) assert.ErrorIs(t, err, ErrInvalidID, "%q", id) } _, err := os.Lstat(filepath.Join(root, "escape.jsonl")) assert.True(t, errors.Is(err, os.ErrNotExist), "nothing created beside the sessions dir") _, err = os.Lstat(filepath.Join(dir, "b.jsonl")) assert.True(t, errors.Is(err, os.ErrNotExist), "no in-dir alias created either") } // Append does no Lstat type check, so O_NONBLOCK is the only thing that keeps // a FIFO named .jsonl with no reader from blocking session_log forever. func TestAppendReturnsPromptlyOnAFIFOLogWithNoReader(t *testing.T) { dir := filepath.Join(t.TempDir(), "sessions") require.NoError(t, os.MkdirAll(dir, 0o755)) fifo := filepath.Join(dir, "s.jsonl") require.NoError(t, syscall.Mkfifo(fifo, 0o644)) done := make(chan error, 1) go func() { done <- Append(dir, "s", Entry{SessionID: "s"}) }() select { case err := <-done: assert.Error(t, err, "a FIFO with no reader is refused, not written") case <-time.After(3 * time.Second): // Unblock the stuck open so the goroutine and TempDir cleanup finish. if r, err := os.OpenFile(fifo, os.O_RDONLY|syscall.O_NONBLOCK, 0); err == nil { defer r.Close() //nolint:errcheck } <-done t.Fatal("Append blocked on a FIFO log with no reader") } } // hyperguild#173: session_log followed a symlinked sessions dir. func TestAppendRefusesASymlinkedSessionsDir(t *testing.T) { root := t.TempDir() elsewhere := filepath.Join(root, "elsewhere") require.NoError(t, os.MkdirAll(elsewhere, 0o755)) dir := filepath.Join(root, "sessions") require.NoError(t, os.Symlink("elsewhere", dir)) assert.Error(t, Append(dir, "s", Entry{SessionID: "s"})) ents, _ := os.ReadDir(elsewhere) assert.Empty(t, ents, "a log was created through the symlinked dir") } // The sessions dir is swapped (RENAME_EXCHANGE) with a symlink to a // directory outside the brain while Append runs. Append opens through an // os.Root on the sessions dir's parent, so a swap can redirect it only // inside that parent (the #167 residual), never outside it. func TestAppendNeverCreatesALogOutsideTheParentUnderADirSwapRace(t *testing.T) { brain := t.TempDir() outside := t.TempDir() dir := filepath.Join(brain, "sessions") other := filepath.Join(brain, "sessions.link") require.NoError(t, os.MkdirAll(dir, 0o755)) require.NoError(t, os.Symlink(outside, other)) var stop atomic.Bool done := make(chan struct{}) go func() { defer close(done) for !stop.Load() { _ = unix.Renameat2(unix.AT_FDCWD, dir, unix.AT_FDCWD, other, unix.RENAME_EXCHANGE) } }() ok := 0 for i := 0; i < 20000; i++ { if Append(dir, fmt.Sprintf("s%d", i), Entry{SessionID: "s"}) == nil { ok++ } } stop.Store(true) <-done ents, _ := os.ReadDir(outside) assert.Empty(t, ents, "logs were created outside the brain") assert.Positive(t, ok, "positive control: appends to the real dir succeed") } // The log file is swapped between a regular file and a symlink to another // file in the brain while Append runs: Append writes only after confirming // the path is the regular file it opened, so the other file never grows. func TestAppendNeverWritesThroughALogSwappedForASymlink(t *testing.T) { brain := t.TempDir() dir := filepath.Join(brain, "sessions") require.NoError(t, os.MkdirAll(dir, 0o755)) victim := filepath.Join(brain, "victim.md") require.NoError(t, os.WriteFile(victim, []byte("untouched\n"), 0o644)) log := filepath.Join(dir, "s.jsonl") var stop atomic.Bool done := make(chan struct{}) go func() { defer close(done) tmp := log + ".swap" for i := 0; !stop.Load(); i++ { _ = os.Remove(tmp) if i%2 == 0 { _ = os.WriteFile(tmp, nil, 0o644) } else { _ = os.Symlink(filepath.Join("..", "victim.md"), tmp) } _ = os.Rename(tmp, log) } }() ok := 0 for i := 0; i < 20000; i++ { if Append(dir, "s", Entry{SessionID: "s"}) == nil { ok++ } } stop.Store(true) <-done got, _ := os.ReadFile(victim) assert.Equal(t, "untouched\n", string(got), "a log line was written through the swapped-in symlink") assert.Positive(t, ok, "positive control: appends to the regular log succeed") }