From 57f9564ce13c25c7a9ce441f3247534207eba249 Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 20 Jul 2026 17:03:20 +0000 Subject: [PATCH] fix(agentcompat): decode inotify events safely Co-authored-by: naiba/CloudCode --- .../internal/process/sqlite_journal_watch.go | 37 +++++++++--- .../process/sqlite_journal_watch_test.go | 60 +++++++++++++++++++ 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/integration/agentcompat/internal/process/sqlite_journal_watch.go b/integration/agentcompat/internal/process/sqlite_journal_watch.go index e5fbef52..2c6aa55f 100644 --- a/integration/agentcompat/internal/process/sqlite_journal_watch.go +++ b/integration/agentcompat/internal/process/sqlite_journal_watch.go @@ -5,11 +5,11 @@ package process import ( "bytes" "context" + "encoding/binary" "errors" "fmt" "path/filepath" "sync" - "unsafe" "golang.org/x/sys/unix" ) @@ -35,6 +35,12 @@ type SQLiteJournalWatch struct { deleted bool } +type inotifyEvent struct { + watchDescriptor int32 + mask uint32 + name []byte +} + func OpenSQLiteJournalWatch(path string) (*SQLiteJournalWatch, error) { journalFD, err := unix.Open(path, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { @@ -140,22 +146,35 @@ func (watch *SQLiteJournalWatch) readEvents() error { if err != nil { return err } - for offset := 0; offset+unix.SizeofInotifyEvent <= count; { - event := (*unix.InotifyEvent)(unsafe.Pointer(&buffer[offset])) - next := offset + unix.SizeofInotifyEvent + int(event.Len) - if next > count { + for offset := 0; offset < count; { + event, consumed, err := decodeInotifyEvent(buffer[offset:count]) + if err != nil { return &SQLiteJournalLifecycleError{} } - nameStart := offset + unix.SizeofInotifyEvent - name := bytes.TrimRight(buffer[nameStart:next], "\x00") - if err := watch.observeEvent(event.Wd, event.Mask, name); err != nil { + if err := watch.observeEvent(event.watchDescriptor, event.mask, event.name); err != nil { return err } - offset = next + offset += consumed } return nil } +func decodeInotifyEvent(buffer []byte) (inotifyEvent, int, error) { + if len(buffer) < unix.SizeofInotifyEvent { + return inotifyEvent{}, 0, errors.New("truncated inotify header") + } + nameLength := binary.NativeEndian.Uint32(buffer[12:]) + if uint64(nameLength) > uint64(len(buffer)-unix.SizeofInotifyEvent) { + return inotifyEvent{}, 0, errors.New("truncated inotify name") + } + consumed64 := uint64(unix.SizeofInotifyEvent) + uint64(nameLength) + if consumed64 > uint64(^uint(0)>>1) { + return inotifyEvent{}, 0, errors.New("inotify event exceeds int range") + } + consumed := int(consumed64) + return inotifyEvent{watchDescriptor: int32(binary.NativeEndian.Uint32(buffer)), mask: binary.NativeEndian.Uint32(buffer[4:]), name: bytes.TrimRight(buffer[unix.SizeofInotifyEvent:consumed], "\x00")}, consumed, nil +} + func (watch *SQLiteJournalWatch) observeEvent(watchDescriptor int32, mask uint32, name []byte) error { if int(watchDescriptor) == watch.directoryWD && mask&unix.IN_DELETE != 0 && bytes.Equal(name, watch.journalName) { return watch.observe(unix.IN_DELETE_SELF) diff --git a/integration/agentcompat/internal/process/sqlite_journal_watch_test.go b/integration/agentcompat/internal/process/sqlite_journal_watch_test.go index ffb0c31e..a9bf4fdd 100644 --- a/integration/agentcompat/internal/process/sqlite_journal_watch_test.go +++ b/integration/agentcompat/internal/process/sqlite_journal_watch_test.go @@ -4,6 +4,7 @@ package process import ( "context" + "encoding/binary" "errors" "os" "path/filepath" @@ -132,6 +133,65 @@ func TestSQLiteJournalWatch_RejectsDuplicateTerminalEvent(t *testing.T) { } } +func TestSQLiteJournalWatch_ReadEventsRejectsTruncatedName(t *testing.T) { + // Given + pipe := make([]int, 2) + requireNoError(t, unix.Pipe(pipe)) + readFD, writeFD := pipe[0], pipe[1] + t.Cleanup(func() { requireNoError(t, unix.Close(readFD)) }) + t.Cleanup(func() { requireNoError(t, unix.Close(writeFD)) }) + watch := &SQLiteJournalWatch{inotifyFD: readFD} + buffer := make([]byte, unix.SizeofInotifyEvent) + binary.NativeEndian.PutUint32(buffer[12:], 1) + _, err := unix.Write(writeFD, buffer) + requireNoError(t, err) + + // When + err = watch.readEvents() + + // Then + if !errors.Is(err, ErrSQLiteJournalLifecycle) { + t.Fatalf("truncated event error=%v, want lifecycle error", err) + } +} + +func TestDecodeInotifyEvent_DecodesUnalignedNativeEndianEvent(t *testing.T) { + // Given + name := []byte("journal\x00\x00") + buffer := append([]byte{0xff}, make([]byte, unix.SizeofInotifyEvent+len(name))...) + eventBytes := buffer[1:] + binary.NativeEndian.PutUint32(eventBytes, 17) + binary.NativeEndian.PutUint32(eventBytes[4:], unix.IN_DELETE) + binary.NativeEndian.PutUint32(eventBytes[12:], uint32(len(name))) + copy(eventBytes[unix.SizeofInotifyEvent:], name) + + // When + event, consumed, err := decodeInotifyEvent(eventBytes) + + // Then + requireNoError(t, err) + if event.watchDescriptor != 17 || event.mask != unix.IN_DELETE || string(event.name) != "journal" { + t.Fatalf("event=%+v", event) + } + if consumed != len(eventBytes) { + t.Fatalf("consumed=%d, want %d", consumed, len(eventBytes)) + } +} + +func TestDecodeInotifyEvent_RejectsTruncatedName(t *testing.T) { + // Given + buffer := make([]byte, unix.SizeofInotifyEvent) + binary.NativeEndian.PutUint32(buffer[12:], 1) + + // When + _, _, err := decodeInotifyEvent(buffer) + + // Then + if err == nil { + t.Fatal("truncated inotify name was accepted") + } +} + func TestSQLiteJournalWatch_WaitsForCloseThenDeleteAndCancellation(t *testing.T) { // Given path := writeJournal(t, "dashboard.sqlite-journal")