274 lines
9.3 KiB
Go
274 lines
9.3 KiB
Go
package winpwn
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
var errOutOfRange = errors.New("byteReaderAt: out of range")
|
|
|
|
// TestDecodeHeapEntryMatchesLiveGroundTruth pins DecodeHeapEntry against
|
|
// three real _HEAP_ENTRY headers + their heap's real Encoding key,
|
|
// captured byte-for-byte from a live process (a tiny HeapAlloc(256)/
|
|
// HeapAlloc(4)/HeapAlloc(16) probe on this machine, Windows build 10.0.26100)
|
|
// and cross-checked against WinDbg's own `!heap -a` ground-truth listing
|
|
// before being trusted -- see heap.go's top comment for the full story.
|
|
//
|
|
// This is the test that caught the real bug worth remembering: the first
|
|
// version of UserSize subtracted HeapEntrySize *again* on top of
|
|
// UnusedBytes (i.e. assumed UnusedBytes was padding *after* a separately-
|
|
// accounted-for header), which silently produced a UserSize 16 bytes
|
|
// smaller than reality for every entry. !heap -a's own "(requested size)"
|
|
// column is what caught it -- UnusedBytes already bakes the header in.
|
|
func TestDecodeHeapEntryMatchesLiveGroundTruth(t *testing.T) {
|
|
// Heap.Encoding, captured at heap+0x80 on the live probe.
|
|
encoding := [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x1e, 0xb6, 0x56, 0xf8, 0xe5, 0xd3, 0x00, 0x00}
|
|
|
|
cases := []struct {
|
|
name string
|
|
addr uint64
|
|
raw [16]byte
|
|
wantSize uint16 // granularity units, cross-checked against `!heap -a`'s byte-size column / HeapEntrySize
|
|
wantFlags uint8
|
|
wantPrevSize uint16
|
|
wantUnused uint8
|
|
wantUserSize uint64 // the requested size `!heap -a` printed in parens
|
|
}{
|
|
{
|
|
// !heap -a: 00000000000d0850: 00110 . 00110 [101] - busy (100)
|
|
name: "256-byte allocation, no slack",
|
|
addr: 0xd0850,
|
|
raw: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0x0f, 0xb6, 0x57, 0xe8, 0xf4, 0xd3, 0x00, 0x10},
|
|
wantSize: 17, // 17*0x10 = 0x110 = 272
|
|
wantFlags: HeapEntryBusy,
|
|
wantPrevSize: 17, // previous entry (0xd0740) was also 0x110 bytes per !heap -a
|
|
wantUnused: 16,
|
|
wantUserSize: 0x100,
|
|
},
|
|
{
|
|
// !heap -a: 00000000000d0d70: 00050 . 00020 [101] - busy (4)
|
|
name: "4-byte allocation, lots of slack",
|
|
addr: 0xd0d70,
|
|
raw: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0x1c, 0xb6, 0x57, 0xfb, 0xe0, 0xd3, 0x00, 0x1c},
|
|
wantSize: 2, // 2*0x10 = 0x20
|
|
wantFlags: HeapEntryBusy,
|
|
wantPrevSize: 5, // previous entry (0xd0d20) was 0x50 bytes per !heap -a
|
|
wantUnused: 28,
|
|
wantUserSize: 4,
|
|
},
|
|
{
|
|
// !heap -a: 00000000000d0d90: 00020 . 00020 [101] - busy (10)
|
|
name: "16-byte allocation, header-only slack",
|
|
addr: 0xd0d90,
|
|
raw: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0x1c, 0xb6, 0x57, 0xfb, 0xe7, 0xd3, 0x00, 0x10},
|
|
wantSize: 2,
|
|
wantFlags: HeapEntryBusy,
|
|
wantPrevSize: 2, // previous entry (0xd0d70) was also 0x20 bytes
|
|
wantUnused: 16,
|
|
wantUserSize: 16,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
e := DecodeHeapEntry(c.addr, c.raw, &encoding)
|
|
if e.Size != c.wantSize {
|
|
t.Errorf("Size = %d, want %d", e.Size, c.wantSize)
|
|
}
|
|
if e.Flags != c.wantFlags {
|
|
t.Errorf("Flags = 0x%x, want 0x%x", e.Flags, c.wantFlags)
|
|
}
|
|
if e.PreviousSize != c.wantPrevSize {
|
|
t.Errorf("PreviousSize = %d, want %d", e.PreviousSize, c.wantPrevSize)
|
|
}
|
|
if e.UnusedBytes != c.wantUnused {
|
|
t.Errorf("UnusedBytes = %d, want %d", e.UnusedBytes, c.wantUnused)
|
|
}
|
|
if got := e.UserSize(); got != c.wantUserSize {
|
|
t.Errorf("UserSize() = 0x%x, want 0x%x", got, c.wantUserSize)
|
|
}
|
|
if !e.Busy() {
|
|
t.Error("Busy() = false, want true (all three live samples were busy)")
|
|
}
|
|
if e.UserData() != c.addr+HeapEntrySize {
|
|
t.Errorf("UserData() = 0x%x, want 0x%x", e.UserData(), c.addr+HeapEntrySize)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectHeapKind(t *testing.T) {
|
|
ntHeap := make([]byte, 0x20)
|
|
binaryLEPutUint32(ntHeap[0x10:], heapSignatureNT)
|
|
|
|
segHeap := make([]byte, 0x20)
|
|
binaryLEPutUint32(segHeap[0x10:], heapSignatureSegment)
|
|
|
|
garbage := make([]byte, 0x20)
|
|
|
|
cases := []struct {
|
|
name string
|
|
buf []byte
|
|
want HeapKind
|
|
ok bool
|
|
}{
|
|
{"nt heap", ntHeap, HeapKindNT, true},
|
|
{"segment heap", segHeap, HeapKindSegment, true},
|
|
{"garbage", garbage, HeapKindUnknown, false},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
kind, err := DetectHeapKind(newByteReaderAt(c.buf), 0)
|
|
if c.ok && err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !c.ok && err == nil {
|
|
t.Fatal("expected an error for an unrecognized signature")
|
|
}
|
|
if kind != c.want {
|
|
t.Errorf("kind = %v, want %v", kind, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestReadHeapDecodesRealCapturedFields pins ReadHeap against the actual
|
|
// field bytes captured from the same live probe heap.go's top comment and
|
|
// heap_test.go's ground-truth test describe (Flags=0x2/HEAP_GROWABLE,
|
|
// EncodeFlagMask=0x100000, Signature=0xeeffeeff -- deliberately distinct
|
|
// from _HEAP_SEGMENT's 0xffeeffee, confirmed with `db` against the live
|
|
// process rather than assumed equal).
|
|
func TestReadHeapDecodesRealCapturedFields(t *testing.T) {
|
|
buf := make([]byte, 0x300)
|
|
binaryLEPutUint32(buf[0x10:], heapSignatureNT) // _HEAP_SEGMENT.SegmentSignature
|
|
binaryLEPutUint32(buf[0x70:], 0x00000002) // Flags = HEAP_GROWABLE
|
|
binaryLEPutUint32(buf[0x7c:], 0x00100000) // EncodeFlagMask
|
|
copy(buf[0x80:0x90], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0x1e, 0xb6, 0x56, 0xf8, 0xe5, 0xd3, 0x00, 0x00})
|
|
binaryLEPutUint32(buf[0x98:], 0xeeffeeff) // _HEAP.Signature
|
|
buf[0x1a2] = FrontEndHeapNone
|
|
binaryLEPutUint64(buf[0x30:], 0x6f0000) // BaseAddress
|
|
binaryLEPutUint64(buf[0x40:], 0x6f0740) // FirstEntry
|
|
binaryLEPutUint64(buf[0x48:], 0x7ef000) // LastValidEntry
|
|
|
|
h, err := ReadHeap(newByteReaderAt(buf), 0)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if h.Signature != 0xeeffeeff {
|
|
t.Errorf("Signature = 0x%x, want 0xeeffeeff", h.Signature)
|
|
}
|
|
if h.Flags != 0x2 {
|
|
t.Errorf("Flags = 0x%x, want 0x2", h.Flags)
|
|
}
|
|
if !h.EncodingActive() {
|
|
t.Error("EncodingActive() = false, want true (EncodeFlagMask is nonzero)")
|
|
}
|
|
if h.FrontEndHeapType != FrontEndHeapNone {
|
|
t.Errorf("FrontEndHeapType = %d, want %d", h.FrontEndHeapType, FrontEndHeapNone)
|
|
}
|
|
if h.BaseAddress != 0x6f0000 || h.FirstEntry != 0x6f0740 || h.LastValidEntry != 0x7ef000 {
|
|
t.Errorf("BaseAddress/FirstEntry/LastValidEntry = 0x%x/0x%x/0x%x", h.BaseAddress, h.FirstEntry, h.LastValidEntry)
|
|
}
|
|
}
|
|
|
|
func TestReadHeapRejectsSegmentHeap(t *testing.T) {
|
|
buf := make([]byte, 0x20)
|
|
binaryLEPutUint32(buf[0x10:], heapSignatureSegment)
|
|
if _, err := ReadHeap(newByteReaderAt(buf), 0); err == nil {
|
|
t.Fatal("expected ReadHeap to reject a Segment Heap signature")
|
|
}
|
|
}
|
|
|
|
func TestHeapSegmentsSingleSegmentIsEmpty(t *testing.T) {
|
|
// A heap with only the embedded Segment0: SegmentList's Flink/Blink
|
|
// both point back at the list head itself (heapOffSegmentList), the
|
|
// standard "empty list" LIST_ENTRY representation.
|
|
buf := make([]byte, 0x200)
|
|
headAddr := uint64(heapOffSegmentList)
|
|
binaryLEPutUint64(buf[heapOffSegmentList:], headAddr)
|
|
binaryLEPutUint64(buf[heapOffSegmentList+8:], headAddr)
|
|
|
|
h := &Heap{Addr: 0}
|
|
segs, err := h.Segments(newByteReaderAt(buf))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(segs) != 0 {
|
|
t.Errorf("got %d segments, want 0 for an empty SegmentList", len(segs))
|
|
}
|
|
}
|
|
|
|
func TestWalkSegmentEntriesStopsAtLastEntry(t *testing.T) {
|
|
// Three unencoded entries (no Encoding key, EncodeFlagMask=0 case):
|
|
// 32 bytes busy, 16 bytes free, 16 bytes busy+LastEntry.
|
|
buf := make([]byte, 64)
|
|
putEntry := func(off int, size uint16, flags uint8, prevSize uint16) {
|
|
buf[off+8] = byte(size)
|
|
buf[off+9] = byte(size >> 8)
|
|
buf[off+10] = flags
|
|
buf[off+12] = byte(prevSize)
|
|
buf[off+13] = byte(prevSize >> 8)
|
|
}
|
|
putEntry(0, 2, HeapEntryBusy, 0)
|
|
putEntry(32, 1, 0, 2)
|
|
putEntry(48, 1, HeapEntryBusy|HeapEntryLastEntry, 1)
|
|
|
|
entries, err := WalkSegmentEntries(newByteReaderAt(buf), 0, 64, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(entries) != 3 {
|
|
t.Fatalf("got %d entries, want 3", len(entries))
|
|
}
|
|
if entries[0].Addr != 0 || entries[0].BlockSize() != 32 || !entries[0].Busy() {
|
|
t.Errorf("entry 0 = %+v", entries[0])
|
|
}
|
|
if entries[1].Addr != 32 || entries[1].BlockSize() != 16 || entries[1].Busy() {
|
|
t.Errorf("entry 1 = %+v", entries[1])
|
|
}
|
|
if entries[2].Addr != 48 || !entries[2].LastEntry() {
|
|
t.Errorf("entry 2 = %+v", entries[2])
|
|
}
|
|
}
|
|
|
|
func TestWalkSegmentEntriesZeroSizeIsAnError(t *testing.T) {
|
|
buf := make([]byte, 32) // entry at 0 decodes to Size=0 -- can't make progress
|
|
_, err := WalkSegmentEntries(newByteReaderAt(buf), 0, 32, nil)
|
|
if err == nil {
|
|
t.Fatal("expected an error for a zero-size entry")
|
|
}
|
|
}
|
|
|
|
// byteReaderAt adapts a plain []byte to io.ReaderAt for synthetic tests,
|
|
// the same role buildSyntheticMinidump's bytes.Reader plays in
|
|
// minidump_test.go.
|
|
type byteReaderAt struct{ buf []byte }
|
|
|
|
func newByteReaderAt(buf []byte) *byteReaderAt { return &byteReaderAt{buf: buf} }
|
|
|
|
func (b *byteReaderAt) ReadAt(p []byte, off int64) (int, error) {
|
|
if off < 0 || int(off) > len(b.buf) {
|
|
return 0, errOutOfRange
|
|
}
|
|
n := copy(p, b.buf[off:])
|
|
if n < len(p) {
|
|
return n, errOutOfRange
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func binaryLEPutUint32(b []byte, v uint32) {
|
|
b[0] = byte(v)
|
|
b[1] = byte(v >> 8)
|
|
b[2] = byte(v >> 16)
|
|
b[3] = byte(v >> 24)
|
|
}
|
|
|
|
func binaryLEPutUint64(b []byte, v uint64) {
|
|
for i := 0; i < 8; i++ {
|
|
b[i] = byte(v >> (8 * i))
|
|
}
|
|
}
|