diff --git a/internal/etw/approvers/fs.go b/internal/etw/approvers/fs.go index 093bb08c7..41dfa38f3 100644 --- a/internal/etw/approvers/fs.go +++ b/internal/etw/approvers/fs.go @@ -85,7 +85,7 @@ type fs struct { } func hashFilePath(u []uint16) uint64 { - b := unsafe.Slice((*byte)(unsafe.Pointer(unsafe.SliceData(u))), len(u)) + b := unsafe.Slice((*byte)(unsafe.Pointer(unsafe.SliceData(u))), len(u)*2) return xxh3.Hash(b) } @@ -178,7 +178,7 @@ func (f *fs) Approve(r *etw.EventRecord) (*etw.EventRecord, bool) { h := hashFilePath(s) path, ok := f.pathLRUCache.Get(h) if !ok { - n := utf16.Decode(s[:len(s)/2-1]) + n := utf16.Decode(s[:max(len(s)-1, 0)]) // drop the null terminator path = devmapper.GetDevMapper().Convert(n) f.pathLRUCache.Add(h, path) } diff --git a/internal/etw/approvers/registry.go b/internal/etw/approvers/registry.go index defe3fd22..1111adb61 100644 --- a/internal/etw/approvers/registry.go +++ b/internal/etw/approvers/registry.go @@ -61,7 +61,7 @@ func newRegistryApprover(r *config.RulesCompileResult) Approver { func hashKeyPath(r string, u []uint16) uint64 { var h xxh3.Hasher - b := unsafe.Slice((*byte)(unsafe.Pointer(unsafe.SliceData(u))), len(u)) + b := unsafe.Slice((*byte)(unsafe.Pointer(unsafe.SliceData(u))), len(u)*2) _, _ = h.Write(b) if r != "" { k := unsafe.Slice(unsafe.StringData(r), len(r)) @@ -109,7 +109,7 @@ func (r *registry) Approve(rec *etw.EventRecord) (*etw.EventRecord, bool) { // KCB object address references a key // we can use to reconstruct the full // registry path - path = utf16.Decode(s[:len(s)/2-1]) + path = utf16.Decode(s[:max(len(s)-1, 0)]) // drop the null terminator if kcb != 0 { path = key.ConcatPaths(r.kcbs[kcb], path) } diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index 2d9fbaf53..050507f47 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -22,6 +22,8 @@ package etw import ( + "bytes" + "encoding/binary" "strings" "unsafe" @@ -564,7 +566,7 @@ type ClassicEventID struct { // IsEmpty indicates if event id has been initialized. func (e ClassicEventID) IsEmpty() bool { - return (e.GUID.Data1 == 0 && e.GUID.Data2 == 0 && e.GUID.Data3 == 0 && len(e.GUID.Data4[:]) == 0) || e.Type == 0 + return e.GUID == (windows.GUID{}) || e.Type == 0 } // EventFilterDescriptor defines the filter data that @@ -627,16 +629,10 @@ func AppendEventHeaderFileExtendedDataItems(r *EventRecord, disposition uint64, // ReadEventHeaderFileExtendedDataItems reads the custom file extended data items from the event record. func (r *EventRecord) ReadEventHeaderFileExtendedDataItems() (uint32, uint32) { - if r.ExtendedData == nil { - return 0, 0 - } - - items := unsafe.Slice(r.ExtendedData, r.ExtendedDataCount) - var disposition uint32 var status uint32 - for _, item := range items { + for _, item := range r.extendedData() { switch item.ExtType { case ExtTypeDisposition: if item.DataSize == 4 && item.DataPtr != 0 { @@ -654,17 +650,15 @@ func (r *EventRecord) ReadEventHeaderFileExtendedDataItems() (uint32, uint32) { // ReadEventHeaderFileExtendedDataItems reads the callstack from the custom file extended data items. func (r *EventRecord) ReadEventHeaderFileExtendedDataItemsCallstack() []va.Address { - if r.ExtendedData == nil { - return nil - } - - items := unsafe.Slice(r.ExtendedData, r.ExtendedDataCount) - for _, item := range items { + for _, item := range r.extendedData() { if item.ExtType != ExtTypeCallstack { continue } n := int(item.DataSize) / 8 + if n == 0 || item.DataPtr == 0 { + return nil + } return unsafe.Slice((*va.Address)(unsafe.Pointer(uintptr(item.DataPtr))), n) } @@ -672,6 +666,15 @@ func (r *EventRecord) ReadEventHeaderFileExtendedDataItemsCallstack() []va.Addre return nil } +// extendedData returns the extended data items attached to the event +// record, or nil if there are none. +func (e *EventRecord) extendedData() []EventHeaderExtendedDataItem { + if e.ExtendedData == nil || e.ExtendedDataCount == 0 { + return nil + } + return unsafe.Slice(e.ExtendedData, int(e.ExtendedDataCount)) +} + // NewClassicEventID creates a new instance of classic event identifier. func NewClassicEventID(guid windows.GUID, typ uint16) ClassicEventID { return ClassicEventID{GUID: guid, Type: uint8(typ)} @@ -691,185 +694,230 @@ func (r *EventRecord) Clone() (*EventRecord, []byte) { *c = *r buf := getBuf(int(r.BufferLen)) - copy(buf, unsafe.Slice((*byte)(unsafe.Pointer(r.Buffer)), r.BufferLen)) + if len(buf) == 0 { + // events without payload have nothing to copy, and + // taking the address of the first element would panic + c.Buffer = 0 + return c, buf + } + copy(buf, r.buf()) c.Buffer = uintptr(unsafe.Pointer(&buf[0])) return c, buf } +// buf returns a view over the raw event payload. The view aliases the ETW +// buffer, so it is only valid for as long as the buffer is alive. All the +// Read* and Consume* methods go through it, so every access is bounds-checked +// against BufferLen by the runtime instead of relying on pointer arithmetic. +func (e *EventRecord) buf() []byte { + if e.Buffer == 0 || e.BufferLen == 0 { + return nil + } + return unsafe.Slice((*byte)(unsafe.Pointer(e.Buffer)), int(e.BufferLen)) +} + +// end returns the offset one past the last byte of the payload. String readers +// return it as the next offset when the requested offset is out of range, so +// chained reads fail closed rather than wrapping around to the payload start. +func (e *EventRecord) end() uint16 { return e.BufferLen } + +// utf16Units returns the UTF-16 code units starting at offset, up to but not +// including the first null terminator, and reports whether a terminator was found. +// If there is no terminator, the units run to the end of the payload. A trailing +// odd byte is ignored. The returned slice aliases the payload. +func (e *EventRecord) utf16Units(offset uint16) ([]uint16, bool) { + b := e.buf() + if int(offset) >= len(b) { + return nil, false + } + n := (len(b) - int(offset)) / 2 + if n == 0 { + return nil, false + } + u := unsafe.Slice((*uint16)(unsafe.Pointer(&b[offset])), n) + for i, c := range u { + if c == 0 { + return u[:i:i], true + } + } + return u, false +} + // ReadByte reads the byte from the buffer at the specified offset. +// Returns zero if the offset is out of range. func (e *EventRecord) ReadByte(offset uint16) byte { - if offset > e.BufferLen { + b := e.buf() + if int(offset) >= len(b) { return 0 } - return *(*byte)(unsafe.Pointer(e.Buffer + uintptr(offset))) + return b[offset] } -// ReadBytes reads a contiguous block of bytes from the buffer. +// ReadBytes reads a contiguous block of bytes from the buffer. The returned +// slice aliases the event buffer, so callers that need the data to outlive +// the buffer must copy it. Returns nil if the requested range is out of bounds. func (e *EventRecord) ReadBytes(offset uint16, count uint16) []byte { - if offset > e.BufferLen { + b := e.buf() + end := int(offset) + int(count) + if end > len(b) { return nil } - return (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:count:count] + return b[offset:end:end] } // ReadUint16 reads the uint16 value from the buffer at the specified offset. +// Returns zero if the value doesn't fit in the buffer. func (e *EventRecord) ReadUint16(offset uint16) uint16 { - if offset > e.BufferLen { + b := e.buf() + if int(offset)+2 > len(b) { return 0 } - return *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(offset))) + return binary.LittleEndian.Uint16(b[offset:]) } // ReadUint32 reads the uint32 value from the buffer at the specified offset. +// Returns zero if the value doesn't fit in the buffer. func (e *EventRecord) ReadUint32(offset uint16) uint32 { - if offset > e.BufferLen { + b := e.buf() + if int(offset)+4 > len(b) { return 0 } - return *(*uint32)(unsafe.Pointer(e.Buffer + uintptr(offset))) + return binary.LittleEndian.Uint32(b[offset:]) } // ReadUint64 reads the uint64 value from the buffer at the specified offset. +// Returns zero if the value doesn't fit in the buffer. func (e *EventRecord) ReadUint64(offset uint16) uint64 { - if offset > e.BufferLen { + b := e.buf() + if int(offset)+8 > len(b) { return 0 } - return *(*uint64)(unsafe.Pointer(e.Buffer + uintptr(offset))) + return binary.LittleEndian.Uint64(b[offset:]) } // ReadAnsiString reads the ANSI string from the buffer at the specified offset. -// Returns the UTF-8 string and the number of bytes read from the string. +// Returns the UTF-8 string and the offset right after the null terminator. If the +// string is not terminated, the remainder of the buffer is consumed. When the +// offset is out of range, an empty string and the end of the buffer are returned. func (e *EventRecord) ReadAnsiString(offset uint16) (string, uint16) { - if offset > e.BufferLen { - return "", 0 + b := e.buf() + if int(offset) >= len(b) { + return "", e.end() } - b := make([]byte, e.BufferLen) - var i uint16 - for i < e.BufferLen { - c := *(*byte)(unsafe.Pointer(e.Buffer + uintptr(offset) + uintptr(i))) - if c == 0 { - break // null terminator - } - b[i] = c - i++ + rest := b[offset:] + n := bytes.IndexByte(rest, 0) + switch { + case n < 0: + return string(rest), e.end() + case n == 0: + return "", offset + 1 // null terminator size + default: + // offset+n is an index inside the buffer, so the + // terminator offset can't overflow the uint16 + return string(rest[:n]), offset + uint16(n) + 1 } - - if i == 0 { - return "", offset + 1 - } - - if int(i) > len(b) { - return string(b[:len(b)-1]), uint16(len(b)) + offset - } - - return string(b[:i]), i + 1 + offset } // ReadUTF16String reads the UTF-16 string from the buffer at the specified offset. -// Returns the UTF-8 string and the number of bytes read from the string + the offset. +// Returns the UTF-8 string and the offset right after the null terminator. If the +// string is not terminated, the remainder of the buffer is consumed. When the +// offset is out of range, an empty string and the end of the buffer are returned. func (e *EventRecord) ReadUTF16String(offset uint16) (string, uint16) { - if offset > e.BufferLen { - return "", 0 - } + units, terminated := e.utf16Units(offset) - // we're reading the leading string. First, calculate - // the length of the null-terminated UTF16 string - i := offset - var length uint16 - for i < e.BufferLen { - c := *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(i))) - if c == 0 { - break // null terminator + if len(units) == 0 { + if terminated { + return "", offset + 2 // null terminator size } - length += 2 - i += 2 + return "", e.end() } - if length == 0 { - return "", offset + 2 // null terminator size + next := e.end() + if terminated { + next = offset + uint16((len(units)+1)*2) } - b := (*[1<<30 - 1]uint16)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] - s := b[:len(b)/2] - - return utf16.Decode(s), uint16((len(s)+1)*2) + offset + return utf16.Decode(units), next } // ReadNTUnicodeString reads the native Unicode string at the given offset. +// Returns the UTF-8 string and the offset of the string null terminator. func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { - if offset > e.BufferLen { - return "", 0 - } + units, terminated := e.utf16Units(offset) - i := offset - var length uint16 - for i < e.BufferLen { - c := *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(i))) - if c == 0 { - break // null terminator + if len(units) == 0 { + if terminated { + return "", offset + 2 // null terminator size } - length += 2 - i += 2 + return "", e.end() } - if length == 0 { - return "", offset + 2 // null terminator size - } - - b := (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] - + size := uint16(len(units) * 2) s := windows.NTUnicodeString{ - Length: uint16(len(b)), - MaximumLength: uint16(len(b)), - Buffer: (*uint16)(unsafe.Pointer(&b[0])), + Length: size, + MaximumLength: size, + Buffer: &units[0], } - return s.String(), s.Length + offset + return s.String(), size + offset } // ConsumeUTF16String reads the byte slice with UTF16-encoded string -// when the UTF16 string is located at the end of the buffer. +// when the UTF16 string is located at the end of the buffer. The last +// code unit is assumed to be the null terminator and is dropped. func (e *EventRecord) ConsumeUTF16String(offset uint16) string { - if offset > e.BufferLen { + s := e.ConsumeRawUTF16String(offset) + if len(s) < 2 { return "" } - s := (*[1<<30 - 1]uint16)(unsafe.Pointer(e.Buffer + uintptr(offset)))[: e.BufferLen-offset : e.BufferLen-offset] - return utf16.Decode(s[:len(s)/2-1]) + return utf16.Decode(s[:len(s)-1]) } -// ConsumeRawUTF16String reads the byte slice for the UTF16-encoded string. +// ConsumeRawUTF16String returns the UTF-16 code units from the given offset to the end +// of the buffer, including the trailing null terminator if present. The slice aliases the +// event buffer and holds exactly (BufferLen-offset)/2 elements. A trailing odd byte is ignored. func (e *EventRecord) ConsumeRawUTF16String(offset uint16) []uint16 { - if offset > e.BufferLen { + b := e.buf() + if int(offset) >= len(b) { return nil } - return (*[1<<30 - 1]uint16)(unsafe.Pointer(e.Buffer + uintptr(offset)))[: e.BufferLen-offset : e.BufferLen-offset] + n := (len(b) - int(offset)) / 2 + if n == 0 { + return nil + } + return unsafe.Slice((*uint16)(unsafe.Pointer(&b[offset])), n) } -// ReadSID reads the security identifier from the event buffer. +// ReadSID reads the security identifier from the event buffer. Returns nil +// if the SID is null or if it doesn't fit in the buffer. func (e *EventRecord) ReadSID(offset uint16, isWbemSid bool) ([]byte, uint16) { + b := e.buf() + // this is a Security Token which can be null and takes 4 bytes. // Otherwise, it is an 8 byte structure (TOKEN_USER) followed by SID, // which is variable size depending on the 2nd byte in the SID - sid := e.ReadUint32(offset) - if sid == 0 { - return nil, offset + 4 + if e.ReadUint32(offset) == 0 { + return nil, uint16(min(int(offset)+4, len(b))) } - var tokenSize uint16 + var tokenSize int if isWbemSid { tokenSize = 16 // TOKEN_USER size } - authorities := e.ReadByte(offset + (tokenSize + 1)) - end := offset + tokenSize + 8 + 4*uint16(authorities) - b := make([]byte, end-offset) - i := offset - for i < end { - b[i-offset] = *(*byte)(unsafe.Pointer(e.Buffer + uintptr(i))) - i++ + start := int(offset) + authorities := int(e.ReadByte(uint16(min(start+tokenSize+1, len(b))))) + end := start + tokenSize + 8 + 4*authorities + if end > len(b) { + return nil, e.end() } - return b, end + + sid := make([]byte, end-start) + copy(sid, b[start:end]) + + return sid, uint16(end) } // ReadCallstack reads callstack return addresses at the given offset. @@ -880,9 +928,12 @@ func (e *EventRecord) ReadCallstack(offset uint16) []va.Address { // ReadCallstackInto reads callstack return addresses at the given offset, // appending into v. If v has sufficient capacity, no allocation occurs. func (e *EventRecord) ReadCallstackInto(offset uint16, v va.Callstack) []va.Address { - var n uint16 - m := offset - frames := (e.BufferLen - offset) / 8 + b := e.buf() + + var frames int + if int(offset) < len(b) { + frames = (len(b) - int(offset)) / 8 + } if v != nil { v = v[:0] @@ -890,10 +941,8 @@ func (e *EventRecord) ReadCallstackInto(offset uint16, v va.Callstack) []va.Addr v = make([]va.Address, 0, frames) } - for n < frames { - v = append(v, va.Address(e.ReadUint64(m))) - m += 8 - n++ + for i := 0; i < frames; i++ { + v = append(v, va.Address(binary.LittleEndian.Uint64(b[int(offset)+i*8:]))) } return v } @@ -913,11 +962,7 @@ type EventExtendedItemStackTrace64 struct { // HasStackTrace determines if the event has the stack trace extended item. func (e *EventRecord) HasStackTrace() bool { - if e.ExtendedDataCount == 0 { - return false - } - data := (*[1 << 23]EventHeaderExtendedDataItem)(unsafe.Pointer(e.ExtendedData))[:e.ExtendedDataCount:e.ExtendedDataCount] - for _, n := range data { + for _, n := range e.extendedData() { if n.ExtType == EventHeaderExtTypeStackTrace64 { return true } @@ -928,20 +973,20 @@ func (e *EventRecord) HasStackTrace() bool { // Callstack collects the event stack trace for // events emitted by non-system logger providers. func (e *EventRecord) Callstack() []va.Address { - if e.ExtendedDataCount == 0 { - return nil - } - data := (*[1 << 23]EventHeaderExtendedDataItem)(unsafe.Pointer(e.ExtendedData))[:e.ExtendedDataCount:e.ExtendedDataCount] - for _, n := range data { + for _, n := range e.extendedData() { if n.ExtType != EventHeaderExtTypeStackTrace64 { continue } - s := (*EventExtendedItemStackTrace64)(unsafe.Pointer(uintptr(n.DataPtr))) - if s == nil { + // the item carries the 8-byte match id followed by the return + // addresses, so anything smaller is malformed. Subtracting from + // a smaller size would wrap around the uint16 and read far past + // the end of the item + if n.DataPtr == 0 || n.DataSize < 8 { continue } + s := (*EventExtendedItemStackTrace64)(unsafe.Pointer(uintptr(n.DataPtr))) // number of return addresses - length := (n.DataSize - 8) / 8 + length := int(n.DataSize-8) / 8 addrs := make([]va.Address, length) addresses := unsafe.Slice(&s.Address[0], length) for i, addr := range addresses { diff --git a/pkg/sys/etw/types_test.go b/pkg/sys/etw/types_test.go index 0ca73f6d5..621d4c0a6 100644 --- a/pkg/sys/etw/types_test.go +++ b/pkg/sys/etw/types_test.go @@ -19,13 +19,35 @@ package etw import ( + "runtime" "testing" "unsafe" + "github.com/rabbitstack/fibratus/pkg/util/va" "github.com/stretchr/testify/assert" "golang.org/x/sys/windows" ) +const canary = 0xFF + +// newRecord returns an event record whose payload is a copy of the given bytes. +// The payload is surrounded by non-zero canary bytes, so a read that strays past +// either end of the payload picks up the canary instead of silently getting a +// zero, and is therefore visible to the assertions. The returned backing slice +// must be kept alive for as long as the record is in use. +func newRecord(payload []byte) (*EventRecord, []byte) { + const pad = 64 + backing := make([]byte, pad+len(payload)+pad) + for i := range backing { + backing[i] = canary + } + copy(backing[pad:], payload) + return &EventRecord{ + BufferLen: uint16(len(payload)), + Buffer: uintptr(unsafe.Pointer(&backing[pad])), + }, backing +} + func TestReadBuffer(t *testing.T) { var tests = []struct { buf []byte @@ -99,3 +121,354 @@ func TestReadBuffer(t *testing.T) { tt.assertions(t, ev) } } + +func TestReadFixedSizeOutOfBounds(t *testing.T) { + payload := []byte{1, 2, 3, 4, 5, 6, 7, 8} + ev, backing := newRecord(payload) + defer runtime.KeepAlive(backing) + + assert.Equal(t, byte(8), ev.ReadByte(7)) + assert.Equal(t, byte(0), ev.ReadByte(8), "offset == BufferLen is one past the end") + assert.Equal(t, byte(0), ev.ReadByte(65535)) + + assert.Equal(t, uint16(0x0201), ev.ReadUint16(0)) + assert.Equal(t, uint16(0), ev.ReadUint16(7), "value straddles the end of the buffer") + assert.Equal(t, uint16(0), ev.ReadUint16(65535)) + + assert.Equal(t, uint32(0x04030201), ev.ReadUint32(0)) + assert.Equal(t, uint32(0), ev.ReadUint32(5), "value straddles the end of the buffer") + assert.Equal(t, uint32(0), ev.ReadUint32(65534)) + + assert.Equal(t, uint64(0x0807060504030201), ev.ReadUint64(0)) + assert.Equal(t, uint64(0), ev.ReadUint64(1), "value straddles the end of the buffer") + assert.Equal(t, uint64(0), ev.ReadUint64(65530)) + + assert.Equal(t, []byte{3, 4}, ev.ReadBytes(2, 2)) + assert.Equal(t, []byte{7, 8}, ev.ReadBytes(6, 2)) + assert.Len(t, ev.ReadBytes(8, 0), 0) + assert.Nil(t, ev.ReadBytes(4, 8), "range extends past the end of the buffer") + assert.Nil(t, ev.ReadBytes(65535, 65535), "offset + count must not wrap around uint16") + + // the returned view must not let callers append into adjacent memory + b := ev.ReadBytes(0, 2) + assert.Equal(t, 2, cap(b)) +} + +func TestReadEmptyAndDetachedBuffers(t *testing.T) { + for _, ev := range []*EventRecord{ + {}, // no payload at all + {BufferLen: 16}, // length without a pointer + {Buffer: 0xdeadbeef}, // pointer without a length + {BufferLen: 0, Buffer: 1}, // zero-length payload + } { + assert.NotPanics(t, func() { + assert.Equal(t, byte(0), ev.ReadByte(0)) + assert.Equal(t, uint64(0), ev.ReadUint64(0)) + assert.Nil(t, ev.ReadBytes(0, 4)) + s, _ := ev.ReadAnsiString(0) + assert.Equal(t, "", s) + s, _ = ev.ReadUTF16String(0) + assert.Equal(t, "", s) + s, _ = ev.ReadNTUnicodeString(0) + assert.Equal(t, "", s) + assert.Equal(t, "", ev.ConsumeUTF16String(0)) + assert.Nil(t, ev.ConsumeRawUTF16String(0)) + sid, _ := ev.ReadSID(0, false) + assert.Nil(t, sid) + assert.Len(t, ev.ReadCallstack(0), 0) + }) + } +} + +func TestReadAnsiString(t *testing.T) { + // terminated string followed by a second one + ev, backing := newRecord([]byte{'c', 'm', 'd', 0, 'x', 0}) + defer runtime.KeepAlive(backing) + + s, next := ev.ReadAnsiString(0) + assert.Equal(t, "cmd", s) + assert.Equal(t, uint16(4), next) + + s, next = ev.ReadAnsiString(next) + assert.Equal(t, "x", s) + assert.Equal(t, uint16(6), next) + + // empty string consumes only the terminator + ev, backing = newRecord([]byte{0, 'a', 0}) + defer runtime.KeepAlive(backing) + s, next = ev.ReadAnsiString(0) + assert.Equal(t, "", s) + assert.Equal(t, uint16(1), next) + + // unterminated string must stop at the end of the buffer instead of + // running into the adjacent memory, regardless of the starting offset + ev, backing = newRecord([]byte{'a', 'b', 'c'}) + defer runtime.KeepAlive(backing) + s, next = ev.ReadAnsiString(0) + assert.Equal(t, "abc", s) + assert.Equal(t, uint16(3), next) + s, next = ev.ReadAnsiString(2) + assert.Equal(t, "c", s) + assert.Equal(t, uint16(3), next) + + // out of range offsets fail closed: the returned offset is the end of the + // buffer, so chained reads don't wrap around to the start of the payload + s, next = ev.ReadAnsiString(3) + assert.Equal(t, "", s) + assert.Equal(t, uint16(3), next) + s, next = ev.ReadAnsiString(65535) + assert.Equal(t, "", s) + assert.Equal(t, uint16(3), next) +} + +func TestReadUTF16String(t *testing.T) { + // "ab\0" "c\0" + ev, backing := newRecord([]byte{'a', 0, 'b', 0, 0, 0, 'c', 0, 0, 0}) + defer runtime.KeepAlive(backing) + + s, next := ev.ReadUTF16String(0) + assert.Equal(t, "ab", s) + assert.Equal(t, uint16(6), next) + + s, next = ev.ReadUTF16String(next) + assert.Equal(t, "c", s) + assert.Equal(t, uint16(10), next) + + // empty string consumes only the terminator + ev, backing = newRecord([]byte{0, 0, 'a', 0}) + defer runtime.KeepAlive(backing) + s, next = ev.ReadUTF16String(0) + assert.Equal(t, "", s) + assert.Equal(t, uint16(2), next) + + // unterminated string with a dangling odd byte must stop at the end of the buffer + ev, backing = newRecord([]byte{'a', 0, 'b', 0, 'c'}) + defer runtime.KeepAlive(backing) + s, next = ev.ReadUTF16String(0) + assert.Equal(t, "ab", s) + assert.Equal(t, uint16(5), next) + + // out of range offsets, including a single trailing byte + s, next = ev.ReadUTF16String(4) + assert.Equal(t, "", s) + assert.Equal(t, uint16(5), next) + s, next = ev.ReadUTF16String(65535) + assert.Equal(t, "", s) + assert.Equal(t, uint16(5), next) +} + +func TestReadNTUnicodeString(t *testing.T) { + ev, backing := newRecord([]byte{'e', 0, 'x', 0, 'e', 0, 0, 0}) + defer runtime.KeepAlive(backing) + + s, next := ev.ReadNTUnicodeString(0) + assert.Equal(t, "exe", s) + assert.Equal(t, uint16(6), next) + + // unterminated, odd length + ev, backing = newRecord([]byte{'e', 0, 'x', 0, 'e'}) + defer runtime.KeepAlive(backing) + s, next = ev.ReadNTUnicodeString(0) + assert.Equal(t, "ex", s) + assert.Equal(t, uint16(4), next) + + s, next = ev.ReadNTUnicodeString(5) + assert.Equal(t, "", s) + assert.Equal(t, uint16(5), next) +} + +func TestConsumeUTF16String(t *testing.T) { + ev, backing := newRecord([]byte{'a', 0, 'b', 0, 0, 0}) + defer runtime.KeepAlive(backing) + + assert.Equal(t, "ab", ev.ConsumeUTF16String(0)) + assert.Equal(t, "b", ev.ConsumeUTF16String(2)) + assert.Equal(t, "", ev.ConsumeUTF16String(4), "only the terminator is left") + + // nothing or less than a code unit left: used to underflow the slice bounds + assert.NotPanics(t, func() { + assert.Equal(t, "", ev.ConsumeUTF16String(5)) + assert.Equal(t, "", ev.ConsumeUTF16String(6)) + assert.Equal(t, "", ev.ConsumeUTF16String(65535)) + }) + + // the raw variant must hold exactly the number of code units that fit + // into the buffer. It used to claim twice as many, which made any + // consumer iterating over it read past the end of the event + assert.Len(t, ev.ConsumeRawUTF16String(0), 3) + assert.Len(t, ev.ConsumeRawUTF16String(1), 2, "odd trailing byte is ignored") + assert.Len(t, ev.ConsumeRawUTF16String(4), 1) + assert.Nil(t, ev.ConsumeRawUTF16String(5)) + assert.Nil(t, ev.ConsumeRawUTF16String(6)) + assert.Nil(t, ev.ConsumeRawUTF16String(65535)) + + raw := ev.ConsumeRawUTF16String(0) + assert.Equal(t, []uint16{'a', 'b', 0}, raw) + assert.Equal(t, 3, cap(raw)) +} + +func TestReadSIDBounds(t *testing.T) { + // S-1-5-18 (local system), a single sub-authority + system := []byte{0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x12, 0x00, 0x00, 0x00} + + ev, backing := newRecord(system) + defer runtime.KeepAlive(backing) + sid, next := ev.ReadSID(0, false) + assert.Equal(t, system, sid) + assert.Equal(t, uint16(12), next) + + // a null security token takes 4 bytes and has no SID + ev, backing = newRecord([]byte{0, 0, 0, 0, 0xAA}) + defer runtime.KeepAlive(backing) + sid, next = ev.ReadSID(0, false) + assert.Nil(t, sid) + assert.Equal(t, uint16(4), next) + + // the sub-authority count claims far more data than the buffer holds + ev, backing = newRecord([]byte{0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05}) + defer runtime.KeepAlive(backing) + sid, next = ev.ReadSID(0, false) + assert.Nil(t, sid) + assert.Equal(t, uint16(8), next) + + // SID is cut off right after the count + ev, backing = newRecord([]byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x12, 0x00, 0x00, 0x00}) + defer runtime.KeepAlive(backing) + sid, _ = ev.ReadSID(0, false) + assert.Nil(t, sid, "two sub-authorities need 16 bytes") + + // the count lives past the end of the buffer + assert.NotPanics(t, func() { + sid, next = ev.ReadSID(10, true) + assert.Nil(t, sid) + sid, next = ev.ReadSID(65535, true) + assert.Nil(t, sid) + assert.Equal(t, uint16(12), next) + }) + + // WBEM SIDs are prefixed by a 16 byte token + wbem := append(make([]byte, 16), system...) + wbem[0] = 0x01 // non-null token + ev, backing = newRecord(wbem) + defer runtime.KeepAlive(backing) + sid, next = ev.ReadSID(0, true) + assert.Equal(t, wbem, sid) + assert.Equal(t, uint16(28), next) +} + +func TestReadCallstackBounds(t *testing.T) { + payload := []byte{ + 1, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, + 0xAA, 0xBB, 0xCC, // dangling bytes, not a full frame + } + ev, backing := newRecord(payload) + defer runtime.KeepAlive(backing) + + assert.Equal(t, []va.Address{1, 2}, ev.ReadCallstack(0)) + assert.Equal(t, []va.Address{2}, ev.ReadCallstack(8)) + assert.Len(t, ev.ReadCallstack(16), 0) + + // offsets past the end used to underflow the frame count + assert.NotPanics(t, func() { + assert.Len(t, ev.ReadCallstack(19), 0) + assert.Len(t, ev.ReadCallstack(20), 0) + assert.Len(t, ev.ReadCallstack(65535), 0) + }) + + // reusing a callstack must start from scratch and keep its backing array + cs := make(va.Callstack, 0, 8) + cs = append(cs, 9, 9, 9) + got := ev.ReadCallstackInto(0, cs) + assert.Equal(t, []va.Address{1, 2}, got) + assert.Equal(t, 8, cap(got)) +} + +func TestCloneEmptyPayload(t *testing.T) { + assert.NotPanics(t, func() { + c, buf := (&EventRecord{}).Clone() + assert.Equal(t, uintptr(0), c.Buffer) + assert.Len(t, buf, 0) + }) + + ev, backing := newRecord([]byte{1, 2, 3, 4}) + defer runtime.KeepAlive(backing) + c, buf := ev.Clone() + assert.Equal(t, []byte{1, 2, 3, 4}, buf) + assert.Equal(t, uint32(0x04030201), c.ReadUint32(0)) + assert.Equal(t, true, c.Buffer != ev.Buffer, "the clone must not alias the original payload") +} + +func TestExtendedDataBounds(t *testing.T) { + // the event claims extended data items, but has no storage for them + ev := &EventRecord{ExtendedDataCount: 2} + assert.NotPanics(t, func() { + assert.Equal(t, false, ev.HasStackTrace()) + assert.Nil(t, ev.Callstack()) + assert.Nil(t, ev.ReadEventHeaderFileExtendedDataItemsCallstack()) + d, s := ev.ReadEventHeaderFileExtendedDataItems() + assert.Equal(t, uint32(0), d) + assert.Equal(t, uint32(0), s) + }) + + // stack trace item: match id followed by two return addresses + stack := []uint64{0xCAFE, 0x1000, 0x2000} + defer runtime.KeepAlive(stack) + item := EventHeaderExtendedDataItem{ + ExtType: EventHeaderExtTypeStackTrace64, + DataSize: uint16(len(stack) * 8), + DataPtr: uint64(uintptr(unsafe.Pointer(&stack[0]))), + } + ev = &EventRecord{ExtendedDataCount: 1, ExtendedData: &item} + assert.Equal(t, true, ev.HasStackTrace()) + assert.Equal(t, []va.Address{0x1000, 0x2000}, ev.Callstack()) + + // a stack trace item that is smaller than its match id is malformed. The + // frame count used to be computed as (DataSize-8)/8 on a uint16, which + // wrapped around and read ~64KB beyond the item + for _, size := range []uint16{0, 4, 7} { + bad := item + bad.DataSize = size + ev = &EventRecord{ExtendedDataCount: 1, ExtendedData: &bad} + assert.NotPanics(t, func() { assert.Nil(t, ev.Callstack(), "size %d", size) }) + } + + // no return addresses, only the match id + empty := item + empty.DataSize = 8 + ev = &EventRecord{ExtendedDataCount: 1, ExtendedData: &empty} + assert.Len(t, ev.Callstack(), 0) + + // null data pointers + null := item + null.DataPtr = 0 + ev = &EventRecord{ExtendedDataCount: 1, ExtendedData: &null} + assert.NotPanics(t, func() { assert.Nil(t, ev.Callstack()) }) + + // custom file extended data items + callstack := []va.Address{0x1000, 0x2000, 0x3000} + ev = &EventRecord{} + f := AppendEventHeaderFileExtendedDataItems(ev, 2, 0xC0000022, callstack) + assert.Equal(t, callstack, ev.ReadEventHeaderFileExtendedDataItemsCallstack()) + disposition, status := ev.ReadEventHeaderFileExtendedDataItems() + assert.Equal(t, uint32(2), disposition) + assert.Equal(t, uint32(0xC0000022), status) + runtime.KeepAlive(f) + + // callstack item pointing nowhere + f.items[2].DataPtr = 0 + ev.ExtendedData, ev.ExtendedDataCount = &f.items[2], 1 + assert.NotPanics(t, func() { assert.Nil(t, ev.ReadEventHeaderFileExtendedDataItemsCallstack()) }) +} + +func TestClassicEventIDIsEmpty(t *testing.T) { + guid := windows.GUID{Data1: 0x3d6fa8d0, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x00, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}} + + assert.Equal(t, true, ClassicEventID{}.IsEmpty()) + assert.Equal(t, true, ClassicEventID{GUID: guid}.IsEmpty(), "no type") + assert.Equal(t, true, ClassicEventID{Type: 1}.IsEmpty(), "no GUID") + assert.Equal(t, true, ClassicEventID{GUID: windows.GUID{Data4: [8]byte{0, 0, 0, 0, 0, 0, 0, 0}}, Type: 1}.IsEmpty()) + assert.Equal(t, false, NewClassicEventID(guid, 1).IsEmpty()) + // a GUID that is zero except for the trailing bytes is still a GUID + assert.Equal(t, false, ClassicEventID{GUID: windows.GUID{Data4: [8]byte{0, 0, 0, 0, 0, 0, 0, 1}}, Type: 1}.IsEmpty()) +}