From 6937fa50b19d82aecaf0d11d904c7822e0e897e7 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Sun, 2 Feb 2025 18:38:15 +1300 Subject: [PATCH 01/29] Moved the generation tag to data-pointer We make this move first because if we have a single-word in a pointer-reference then we only have one place to keep the generation tag. The single word will be the data-pointer. --- offheap/internal/pointerstore/pointer_reference.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 8fa0fbd..0757ecd 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -14,7 +14,7 @@ const genMask = uint64(0xFF << maskShift) const pointerMask = ^genMask // The address field holds a pointer to an object, but also sneaks a generation -// value in the top 8 bits of the metaAddress field. +// value in the top 8 bits of the dataAddress field. // // The generation must be masked out to get a usable pointer value. The object // pointed to must have the same generation value in order to access/free that @@ -159,12 +159,12 @@ func (r *RefPointer) metadata() *metadata { //gcassert:noescape func (r *RefPointer) Gen() uint8 { - return (uint8)((r.metaAddress & genMask) >> maskShift) + return (uint8)((r.dataAddress & genMask) >> maskShift) } //gcassert:noescape func (r *RefPointer) setGen(gen uint8) { - r.metaAddress = (r.metaAddress & pointerMask) | (uint64(gen) << maskShift) + r.dataAddress = (r.dataAddress & pointerMask) | (uint64(gen) << maskShift) } // This method re-allocates the memory location. When this method returns r From 4e41d47dbd4272f0feff1d78aa5a7a1d7486408c Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Sun, 2 Feb 2025 18:41:27 +1300 Subject: [PATCH 02/29] Rename dataAddress to dataAddressAndGen This makes it clearer where the generation tag is being smuggled. It also alerts the reader that directly using this field will use more data than just a dataAddress. --- offheap/internal/pointerstore/pointer_reference.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 0757ecd..34997d6 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -31,8 +31,8 @@ const pointerMask = ^genMask // is a meaningful improvement are speculative and haven't been tested. This // would be a good target for future performance testing. type RefPointer struct { - dataAddress uint64 - metaAddress uint64 + dataAddressAndGen uint64 + metaAddress uint64 } // If the object's metadata has a non-nil nextFree pointer then the object is @@ -66,8 +66,8 @@ func NewReference(pAddress, pMetadata uintptr) RefPointer { // NB: The gen on a brand new Reference is always 0 // So we don't set it return RefPointer{ - dataAddress: maskedAddress, - metaAddress: uint64(pMetadata), + dataAddressAndGen: maskedAddress, + metaAddress: uint64(pMetadata), } } @@ -136,7 +136,7 @@ func (r *RefPointer) DataPtr() uintptr { if meta.gen != r.Gen() { panic(fmt.Errorf("attempt to get value (%d) using stale reference (%d)", meta.gen, r.Gen())) } - return (uintptr)(r.dataAddress & pointerMask) + return (uintptr)(r.dataAddressAndGen & pointerMask) } // Convenient method to retrieve raw data of an allocation @@ -159,12 +159,12 @@ func (r *RefPointer) metadata() *metadata { //gcassert:noescape func (r *RefPointer) Gen() uint8 { - return (uint8)((r.dataAddress & genMask) >> maskShift) + return (uint8)((r.dataAddressAndGen & genMask) >> maskShift) } //gcassert:noescape func (r *RefPointer) setGen(gen uint8) { - r.dataAddress = (r.dataAddress & pointerMask) | (uint64(gen) << maskShift) + r.dataAddressAndGen = (r.dataAddressAndGen & pointerMask) | (uint64(gen) << maskShift) } // This method re-allocates the memory location. When this method returns r From ecc2da8bc714cb4f25f76efc4661fd65eea74ffd Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Sun, 2 Feb 2025 19:15:11 +1300 Subject: [PATCH 03/29] metadata now has dataAddressAndGen field This replaces the old 'gen uint8' we had before. The behaviour does not change in this commit, we are simply storing the generation tag in a larger field than before. The next steps will be to actually use this new full address to do the data lookup. Also added a helper function to test the validity of a metadata/reference pair. This functionality was being duplicated between Free() and DataPtr() methods. --- .../pointerstore/pointer_reference.go | 91 ++++++++++++------- .../pointerstore/pointer_reference_test.go | 15 ++- 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 34997d6..656ef3c 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -43,32 +43,56 @@ type RefPointer struct { // value can access/free objects they point to. This is a best-effort safety // check to try to catch use-after-free type errors. type metadata struct { - nextFree RefPointer - gen uint8 + nextFree RefPointer + dataAddressAndGen uint64 +} + +//gcassert:noescape +func (m *metadata) gen() uint8 { + return (uint8)((m.dataAddressAndGen & genMask) >> maskShift) } -func NewReference(pAddress, pMetadata uintptr) RefPointer { - if pAddress == (uintptr)(unsafe.Pointer(nil)) { - panic("cannot create new Reference with nil pointer") +//gcassert:noescape +func (m *metadata) setGen(gen uint8) { + m.dataAddressAndGen = (m.dataAddressAndGen & pointerMask) | (uint64(gen) << maskShift) +} + +// Check that the metadata for a reference agrees with the generation tag and the data address. +// Failure results in a panic. +// +//gcassert:noescape +func (m *metadata) checkReference(r *RefPointer) { + if m.gen() != r.Gen() { + panic(fmt.Errorf("attempt to get value (%d) using stale reference (%d)", m.gen(), r.Gen())) } - address := uint64(pAddress) - // This sets the generation to 0 by clearing the smuggled bits - maskedAddress := address & pointerMask + if m.dataAddressAndGen != r.dataAddressAndGen { + panic(fmt.Errorf("attempt to get value where reference's data-address (%d) and metadata's data-address (%d) are different", r.dataAddressAndGen, m.dataAddressAndGen)) + } +} - // Setting the generation 0 shouldn't actually change the address - // If there were any 1s in the top part of the address our generation - // smuggling system will break this pointer. This is an unrecoverable error. - if address != maskedAddress { - panic(fmt.Errorf("the raw pointer (%d) uses more than %d bits", address, maskShift)) +func NewReference(dataAddress, metaAddress uintptr) RefPointer { + if dataAddress == (uintptr)(unsafe.Pointer(nil)) { + panic("cannot create new Reference with nil data pointer") } - // NB: The gen on a brand new Reference is always 0 - // So we don't set it - return RefPointer{ - dataAddressAndGen: maskedAddress, - metaAddress: uint64(pMetadata), + r := RefPointer{ + dataAddressAndGen: uint64(dataAddress), + metaAddress: uint64(metaAddress), } + + if r.Gen() != 0 { + panic(fmt.Errorf("the data pointer (%d) contains a non-zero generation tag (%d)", dataAddress, r.Gen())) + } + + // Set the dataAddressAndGen in this reference's metadata. + // + // In one of the next steps we will use this to look up the actual data + // _and_ verify the generation of the reference. + meta := r.metadata() + meta.dataAddressAndGen = r.dataAddressAndGen + + return r } //gcassert:noescape @@ -84,10 +108,12 @@ func (r *RefPointer) AllocFromFree() (nextFree RefPointer) { nextFree = RefPointer{} } - // Increment the generation for the object and set that generation in - // the Reference - meta.gen++ - r.setGen(meta.gen) + // Increment the generation for the allocation and set that generation in + // the Metadata and Reference + gen := meta.gen() + gen++ + meta.setGen(gen) + r.setGen(gen) return nextFree } @@ -102,9 +128,7 @@ func (r *RefPointer) Free(oldFree RefPointer) { panic(fmt.Errorf("attempted to Free freed allocation %v", *r)) } - if meta.gen != r.Gen() { - panic(fmt.Errorf("attempt to free allocation (%d) using stale reference (%d)", meta.gen, r.Gen())) - } + meta.checkReference(r) if oldFree.IsNil() { meta.nextFree = *r @@ -130,12 +154,11 @@ func (r *RefPointer) DataPtr() uintptr { // call, but if we don't take a copy of r in the fmt call, then // every call will allocate regardless of whether the method // panics or not - panic(fmt.Errorf("attempted to get freed allocation %v", *r)) + panic(fmt.Errorf("attempt to get freed allocation %v", *r)) } - if meta.gen != r.Gen() { - panic(fmt.Errorf("attempt to get value (%d) using stale reference (%d)", meta.gen, r.Gen())) - } + meta.checkReference(r) + return (uintptr)(r.dataAddressAndGen & pointerMask) } @@ -172,8 +195,14 @@ func (r *RefPointer) setGen(gen uint8) { // valid reference to the same location. func (r *RefPointer) Realloc() RefPointer { newRef := *r + + // Get metadata generation tag and increment it meta := r.metadata() - meta.gen++ - newRef.setGen(meta.gen) + gen := meta.gen() + gen++ + + // Set the new generation tag in both the metadata and reference + meta.setGen(gen) + newRef.setGen(gen) return newRef } diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index 96e859a..7adc1da 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -22,7 +22,7 @@ func TestNewReferenceWithNilPanics(t *testing.T) { } // Demonstrate that a pointer with any non-0 field is not nil -func TestIsNotNil(t *testing.T) { +func TestNewReference(t *testing.T) { allocConfig := NewAllocConfigBySize(8, 32*8) objects, metadata := MmapSlab(allocConfig) for i := range objects { @@ -35,6 +35,9 @@ func TestIsNotNil(t *testing.T) { assert.Equal(t, metadata[i], r.metadataPtr()) // Generation of a new Reference is always 0 assert.Equal(t, uint8(0), r.Gen()) + // The dataAddressAndGen matches in both the reference and the metadata + meta := r.metadata() + assert.Equal(t, r.dataAddressAndGen, meta.dataAddressAndGen) } } @@ -62,7 +65,7 @@ func TestGenerationDoesNotAppearInOtherFields(t *testing.T) { metadata := r.metadata() gen := uint8(255) - metadata.gen = gen + metadata.setGen(gen) r.setGen(gen) assert.Equal(t, dataPtr, r.DataPtr()) @@ -75,11 +78,19 @@ func TestRealloc(t *testing.T) { objects, metadatas := MmapSlab(allocConfig) r1 := NewReference(objects[0], metadatas[0]) + meta1 := r1.metadata() + dataPtr := r1.DataPtr() metaPtr := r1.metadataPtr() gen := r1.Gen() r2 := r1.Realloc() + meta2 := r2.metadata() + + // The dataAddressAndGen matches in both the reference and the metadata + assert.Equal(t, r2.dataAddressAndGen, meta2.dataAddressAndGen) + // The dataAddressAndGen of the original metadata has been updated to match r2 + assert.Equal(t, r2.dataAddressAndGen, meta1.dataAddressAndGen) // Assert that the data/metadata pointed to by r1 and r2 is the same assert.Equal(t, dataPtr, r2.DataPtr()) From 441c23122fcaead6c8b9f13b2aed8b20c3f8e881 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Sun, 2 Feb 2025 19:18:33 +1300 Subject: [PATCH 04/29] NewReference panics if nil metaAddress is provided This was an oversight, we were checking the dataAddress for nil but didn't check the metadata. --- offheap/internal/pointerstore/pointer_reference.go | 4 ++++ .../internal/pointerstore/pointer_reference_test.go | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 656ef3c..1998ea9 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -76,6 +76,10 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { panic("cannot create new Reference with nil data pointer") } + if metaAddress == (uintptr)(unsafe.Pointer(nil)) { + panic("cannot create new Reference with nil metadata pointer") + } + r := RefPointer{ dataAddressAndGen: uint64(dataAddress), metaAddress: uint64(metaAddress), diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index 7adc1da..e1008ad 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -6,6 +6,7 @@ package pointerstore import ( "testing" + "unsafe" "github.com/stretchr/testify/assert" ) @@ -16,9 +17,16 @@ func TestIsNil(t *testing.T) { assert.True(t, r.IsNil()) } -// Calling newReference() with nil will panic +// Calling newReference() with any nil uintptr parameter will panic func TestNewReferenceWithNilPanics(t *testing.T) { - assert.Panics(t, func() { NewReference(0, 0) }) + nilPtr := uintptr(unsafe.Pointer(nil)) + + allocConfig := NewAllocConfigBySize(8, 8) + objects, metadata := MmapSlab(allocConfig) + + assert.Panics(t, func() { NewReference(objects[0], nilPtr) }) + assert.Panics(t, func() { NewReference(nilPtr, metadata[0]) }) + assert.Panics(t, func() { NewReference(nilPtr, nilPtr) }) } // Demonstrate that a pointer with any non-0 field is not nil From cf1e21325dda42eea90da8a441f2d999ed44540d Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Sun, 2 Feb 2025 19:36:34 +1300 Subject: [PATCH 05/29] Add taggedAddress type for RefPointer The tagged address wraps up the address and generation tag into a single type sized uint64. This cleans up some code and allows us to separate out this tagging code from the RefPointer code. --- .../pointerstore/pointer_reference.go | 35 ++++----- .../pointerstore/pointer_reference_test.go | 13 ++++ .../internal/pointerstore/tagged_address.go | 44 +++++++++++ .../pointerstore/tagged_address_test.go | 75 +++++++++++++++++++ 4 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 offheap/internal/pointerstore/tagged_address.go create mode 100644 offheap/internal/pointerstore/tagged_address_test.go diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 1998ea9..edb3685 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -9,10 +9,6 @@ import ( "unsafe" ) -const maskShift = 56 // This leaves 8 bits for the generation data -const genMask = uint64(0xFF << maskShift) -const pointerMask = ^genMask - // The address field holds a pointer to an object, but also sneaks a generation // value in the top 8 bits of the dataAddress field. // @@ -31,8 +27,8 @@ const pointerMask = ^genMask // is a meaningful improvement are speculative and haven't been tested. This // would be a good target for future performance testing. type RefPointer struct { - dataAddressAndGen uint64 - metaAddress uint64 + dataAddressAndGen taggedAddress + metaAddress taggedAddress } // If the object's metadata has a non-nil nextFree pointer then the object is @@ -44,17 +40,17 @@ type RefPointer struct { // check to try to catch use-after-free type errors. type metadata struct { nextFree RefPointer - dataAddressAndGen uint64 + dataAddressAndGen taggedAddress } //gcassert:noescape func (m *metadata) gen() uint8 { - return (uint8)((m.dataAddressAndGen & genMask) >> maskShift) + return m.dataAddressAndGen.gen() } //gcassert:noescape func (m *metadata) setGen(gen uint8) { - m.dataAddressAndGen = (m.dataAddressAndGen & pointerMask) | (uint64(gen) << maskShift) + m.dataAddressAndGen = m.dataAddressAndGen.withGen(gen) } // Check that the metadata for a reference agrees with the generation tag and the data address. @@ -81,12 +77,8 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { } r := RefPointer{ - dataAddressAndGen: uint64(dataAddress), - metaAddress: uint64(metaAddress), - } - - if r.Gen() != 0 { - panic(fmt.Errorf("the data pointer (%d) contains a non-zero generation tag (%d)", dataAddress, r.Gen())) + dataAddressAndGen: newTaggedAddress(dataAddress), + metaAddress: newTaggedAddress(metaAddress), } // Set the dataAddressAndGen in this reference's metadata. @@ -143,7 +135,7 @@ func (r *RefPointer) Free(oldFree RefPointer) { //gcassert:noescape func (r *RefPointer) IsNil() bool { - return r.metadataPtr() == 0 + return r.dataAddressAndGen.isNil() } //gcassert:noescape @@ -163,20 +155,19 @@ func (r *RefPointer) DataPtr() uintptr { meta.checkReference(r) - return (uintptr)(r.dataAddressAndGen & pointerMask) + return r.dataAddressAndGen.address() } // Convenient method to retrieve raw data of an allocation // //gcassert:noescape func (r *RefPointer) Bytes(size int) []byte { - ptr := r.DataPtr() - return pointerToBytes(ptr, size) + return r.dataAddressAndGen.bytes(size) } //gcassert:noescape func (r *RefPointer) metadataPtr() uintptr { - return (uintptr)(r.metaAddress & pointerMask) + return r.metaAddress.address() } //gcassert:noescape @@ -186,12 +177,12 @@ func (r *RefPointer) metadata() *metadata { //gcassert:noescape func (r *RefPointer) Gen() uint8 { - return (uint8)((r.dataAddressAndGen & genMask) >> maskShift) + return r.dataAddressAndGen.gen() } //gcassert:noescape func (r *RefPointer) setGen(gen uint8) { - r.dataAddressAndGen = (r.dataAddressAndGen & pointerMask) | (uint64(gen) << maskShift) + r.dataAddressAndGen = r.dataAddressAndGen.withGen(gen) } // This method re-allocates the memory location. When this method returns r diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index e1008ad..e14d4b7 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -29,6 +29,19 @@ func TestNewReferenceWithNilPanics(t *testing.T) { assert.Panics(t, func() { NewReference(nilPtr, nilPtr) }) } +// Calling newReference() with any uintptr parameter with generation tag bits set will panic +func TestNewReferenceWithGenerationBitsSetPanics(t *testing.T) { + allocConfig := NewAllocConfigBySize(8, 8) + objects, metadata := MmapSlab(allocConfig) + + badObject := objects[0] | (1 << maskShift) + badMeta := metadata[0] | (1 << maskShift) + + assert.Panics(t, func() { NewReference(objects[0], badMeta) }) + assert.Panics(t, func() { NewReference(badObject, metadata[0]) }) + assert.Panics(t, func() { NewReference(badObject, badMeta) }) +} + // Demonstrate that a pointer with any non-0 field is not nil func TestNewReference(t *testing.T) { allocConfig := NewAllocConfigBySize(8, 32*8) diff --git a/offheap/internal/pointerstore/tagged_address.go b/offheap/internal/pointerstore/tagged_address.go new file mode 100644 index 0000000..8e52be9 --- /dev/null +++ b/offheap/internal/pointerstore/tagged_address.go @@ -0,0 +1,44 @@ +// Copyright 2025 Francis Michael Stephens. All rights reserved. Use of this +// source code is governed by an MIT license that can be found in the LICENSE +// file. + +package pointerstore + +import ( + "fmt" + "unsafe" +) + +const maskShift = 56 // This leaves 8 bits for the generation data +const genMask = taggedAddress(0xFF << maskShift) +const pointerMask = ^genMask + +type taggedAddress uint64 + +func newTaggedAddress(ptr uintptr) taggedAddress { + ta := taggedAddress(ptr) + if ta.gen() != 0 { + panic(fmt.Errorf("Cannot create tagged address from pointer (%d) with bits set in the generation tag (%d).", ptr, ta.gen())) + } + return ta +} + +func (a taggedAddress) gen() uint8 { + return (uint8)((a & genMask) >> maskShift) +} + +func (a taggedAddress) address() uintptr { + return uintptr(a & pointerMask) +} + +func (a taggedAddress) withGen(gen uint8) taggedAddress { + return (a & pointerMask) | (taggedAddress(gen) << maskShift) +} + +func (a taggedAddress) bytes(size int) []byte { + return ([]byte)(unsafe.Slice((*byte)((unsafe.Pointer)(a.address())), size)) +} + +func (a taggedAddress) isNil() bool { + return a.address() == 0 +} diff --git a/offheap/internal/pointerstore/tagged_address_test.go b/offheap/internal/pointerstore/tagged_address_test.go new file mode 100644 index 0000000..b079eee --- /dev/null +++ b/offheap/internal/pointerstore/tagged_address_test.go @@ -0,0 +1,75 @@ +// Copyright 2025 Francis Michael Stephens. All rights reserved. Use of this +// source code is governed by an MIT license that can be found in the LICENSE +// file. + +package pointerstore + +import ( + "math/rand" + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" +) + +const addressShift = 64 - maskShift +const maxAddress = (1 << addressShift) - 1 + +// For a nil uintptr the address and generation tag are zero +func TestTaggedAddress_ZeroValue(t *testing.T) { + var ta taggedAddress + + // address and gen are 0 + assert.True(t, ta.isNil()) + assert.Equal(t, uintptr(unsafe.Pointer(nil)), ta.address()) + assert.Equal(t, uint8(0), ta.gen()) +} + +// For a nil uintptr the address and generation tag are zero +func TestTaggedAddress_NewTaggedAddress_Zero(t *testing.T) { + nilPtr := uintptr(unsafe.Pointer(nil)) + ta := newTaggedAddress(nilPtr) + + // address and gen are 0 + assert.True(t, ta.isNil()) + assert.Equal(t, nilPtr, ta.address()) + assert.Equal(t, uint8(0), ta.gen()) +} + +// For any legal address and generation tag combination the address and +// generation tag are available unaltered +func TestTaggedAddress_AddressAndGenAreSeparate(t *testing.T) { + for range 10 { + newPtr := uintptr(rand.Int63n(maxAddress + 1)) + ta := newTaggedAddress(newPtr) + + for i := 1; i <= 255; i++ { + gen := uint8(i) + // set tagged address generation + ta = ta.withGen(gen) + + // If the address is not 0 then isNil() must be false + assert.Equal(t, newPtr == 0, ta.isNil()) + // Assert that the original address is preserved + assert.Equal(t, newPtr, ta.address()) + // Assert that the generation is preserved + assert.Equal(t, gen, ta.gen()) + } + } +} + +// For any address with bits set in the generation tag region (i.e. upper 8 +// bits) newTaggedAddress() panics +func TestTaggedAddress_PointerWithGenBitsPanics(t *testing.T) { + for range 10 { + newPtr := uintptr(rand.Int63n(maxAddress + 1)) + + //for genBits := uintptr(1); genBits <= uintptr(255); genBits++ { + for i := 1; i <= 255; i++ { + genBits := uintptr(i) + // Create a pointer with bits set in the gen tag bits + badPtr := newPtr | ((genBits) << maskShift) + assert.Panics(t, func() { newTaggedAddress(badPtr) }) + } + } +} From 8dde2367621ba22a92bedfd9347de9fdebb781b2 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Sun, 2 Feb 2025 19:39:40 +1300 Subject: [PATCH 06/29] checkReference() checks only the raw value Previously we separately checked that the generation tags were the same. But this is redundant. Now we just check that the raw address values are the same and print an explanatory error in the panic including the taggedAddress values as hex. This should be enough to debug and error in this case. Just printing big raw hex values _could_ be harder to debug for users who are not already familiar with the internals of the offheap package. Maybe a more beginner friendly message could be used instead. --- offheap/internal/pointerstore/pointer_reference.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index edb3685..ab433aa 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -58,12 +58,10 @@ func (m *metadata) setGen(gen uint8) { // //gcassert:noescape func (m *metadata) checkReference(r *RefPointer) { - if m.gen() != r.Gen() { - panic(fmt.Errorf("attempt to get value (%d) using stale reference (%d)", m.gen(), r.Gen())) - } - if m.dataAddressAndGen != r.dataAddressAndGen { - panic(fmt.Errorf("attempt to get value where reference's data-address (%d) and metadata's data-address (%d) are different", r.dataAddressAndGen, m.dataAddressAndGen)) + const hexWidth = 16 + panic(fmt.Errorf("attempt to get value where reference's taggedAddress (%#0*X) and metadata's taggedAddress (%#0*X) are different", hexWidth, r.dataAddressAndGen, hexWidth, m.dataAddressAndGen)) + } } From 880f6b32acd9ff34e5fb95265ef234b273d26566 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Mon, 3 Feb 2025 18:15:24 +1300 Subject: [PATCH 07/29] checkReference produces specific panic messages Because checkReference can fail if the generations _or_ the addresses are different between a RefPointer and it's associated metadata, we generate specific panic messages for each of these cases. Hopefully this makes debugging easier for library users. --- .../pointerstore/pointer_reference.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index ab433aa..9a0fcab 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -59,9 +59,24 @@ func (m *metadata) setGen(gen uint8) { //gcassert:noescape func (m *metadata) checkReference(r *RefPointer) { if m.dataAddressAndGen != r.dataAddressAndGen { - const hexWidth = 16 - panic(fmt.Errorf("attempt to get value where reference's taggedAddress (%#0*X) and metadata's taggedAddress (%#0*X) are different", hexWidth, r.dataAddressAndGen, hexWidth, m.dataAddressAndGen)) + mGen := m.gen() + rGen := r.Gen() + mAddress := m.dataAddressAndGen.address() + rAddress := r.dataAddressAndGen.address() + genMismatchMessage := fmt.Sprintf("generation mismatch between metadata (%d) and reference (%d)", m.gen(), r.Gen()) + addressMismatchMessage := fmt.Sprintf("address mismatch between metadata (%#0*X) and reference (%#0*X)", 14, m.dataAddressAndGen.address(), 14, r.dataAddressAndGen.address()) + + switch { + case mGen != rGen && mAddress != rAddress: + panic(fmt.Errorf(genMismatchMessage + " and " + addressMismatchMessage)) + + case mGen != rGen: + panic(fmt.Errorf(genMismatchMessage)) + + case mAddress != rAddress: + panic(fmt.Errorf(addressMismatchMessage)) + } } } From 45fada8ad3fc7e3c5f69e60f0823b2866dd1b340 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Mon, 3 Feb 2025 18:24:52 +1300 Subject: [PATCH 08/29] Move metadata into its own file. This makes no significant change, but attempts to make the code easier to reason about. In the next steps the RefPointer code shouldn't know about metadata explicitly at all. --- offheap/internal/pointerstore/metadata.go | 56 +++++++++++++++++++ .../pointerstore/pointer_reference.go | 49 ---------------- 2 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 offheap/internal/pointerstore/metadata.go diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go new file mode 100644 index 0000000..cf8bede --- /dev/null +++ b/offheap/internal/pointerstore/metadata.go @@ -0,0 +1,56 @@ +// Copyright 2025 Francis Michael Stephens. All rights reserved. Use of this +// source code is governed by an MIT license that can be found in the LICENSE +// file. + +package pointerstore + +import "fmt" + +// If the object's metadata has a non-nil nextFree pointer then the object is +// currently free. Object's which have never been allocated are implicitly +// free, but have a nil nextFree. +// +// A RefPointer smuggles a generation tag. Only references with the same gen +// value can access/free objects they point to. This is a best-effort safety +// check to try to catch use-after-free type errors. +type metadata struct { + nextFree RefPointer + dataAddressAndGen taggedAddress +} + +//gcassert:noescape +func (m *metadata) gen() uint8 { + return m.dataAddressAndGen.gen() +} + +//gcassert:noescape +func (m *metadata) setGen(gen uint8) { + m.dataAddressAndGen = m.dataAddressAndGen.withGen(gen) +} + +// Check that the metadata for a reference agrees with the generation tag and the data address. +// Failure results in a panic. +// +//gcassert:noescape +func (m *metadata) checkReference(r *RefPointer) { + if m.dataAddressAndGen != r.dataAddressAndGen { + mGen := m.gen() + rGen := r.Gen() + mAddress := m.dataAddressAndGen.address() + rAddress := r.dataAddressAndGen.address() + + genMismatchMessage := fmt.Sprintf("generation mismatch between metadata (%d) and reference (%d)", m.gen(), r.Gen()) + addressMismatchMessage := fmt.Sprintf("address mismatch between metadata (%#0*X) and reference (%#0*X)", 14, m.dataAddressAndGen.address(), 14, r.dataAddressAndGen.address()) + + switch { + case mGen != rGen && mAddress != rAddress: + panic(fmt.Errorf(genMismatchMessage + " and " + addressMismatchMessage)) + + case mGen != rGen: + panic(fmt.Errorf(genMismatchMessage)) + + case mAddress != rAddress: + panic(fmt.Errorf(addressMismatchMessage)) + } + } +} diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 9a0fcab..d836c5d 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -31,55 +31,6 @@ type RefPointer struct { metaAddress taggedAddress } -// If the object's metadata has a non-nil nextFree pointer then the object is -// currently free. Object's which have never been allocated are implicitly -// free, but have a nil nextFree. -// -// An object's metadata has a gen field. Only references with the same gen -// value can access/free objects they point to. This is a best-effort safety -// check to try to catch use-after-free type errors. -type metadata struct { - nextFree RefPointer - dataAddressAndGen taggedAddress -} - -//gcassert:noescape -func (m *metadata) gen() uint8 { - return m.dataAddressAndGen.gen() -} - -//gcassert:noescape -func (m *metadata) setGen(gen uint8) { - m.dataAddressAndGen = m.dataAddressAndGen.withGen(gen) -} - -// Check that the metadata for a reference agrees with the generation tag and the data address. -// Failure results in a panic. -// -//gcassert:noescape -func (m *metadata) checkReference(r *RefPointer) { - if m.dataAddressAndGen != r.dataAddressAndGen { - mGen := m.gen() - rGen := r.Gen() - mAddress := m.dataAddressAndGen.address() - rAddress := r.dataAddressAndGen.address() - - genMismatchMessage := fmt.Sprintf("generation mismatch between metadata (%d) and reference (%d)", m.gen(), r.Gen()) - addressMismatchMessage := fmt.Sprintf("address mismatch between metadata (%#0*X) and reference (%#0*X)", 14, m.dataAddressAndGen.address(), 14, r.dataAddressAndGen.address()) - - switch { - case mGen != rGen && mAddress != rAddress: - panic(fmt.Errorf(genMismatchMessage + " and " + addressMismatchMessage)) - - case mGen != rGen: - panic(fmt.Errorf(genMismatchMessage)) - - case mAddress != rAddress: - panic(fmt.Errorf(addressMismatchMessage)) - } - } -} - func NewReference(dataAddress, metaAddress uintptr) RefPointer { if dataAddress == (uintptr)(unsafe.Pointer(nil)) { panic("cannot create new Reference with nil data pointer") From 87075932331eef72cdfed6946e35089992cf70a6 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Mon, 3 Feb 2025 18:26:33 +1300 Subject: [PATCH 09/29] Replace linked-list of free objects with slice This is a much simpler implementation. It also removed object meta-data from the critical path of tracking free objects. Now meta-data _only_ checks if the object is free or if the object has the right generation tag for the accessing reference. These functions are useful, but not essential, so the meta-data can be made optional. --- offheap/internal/pointerstore/metadata.go | 2 +- .../pointerstore/pointer_reference.go | 29 +++++++------------ .../internal/pointerstore/pointer_store.go | 21 +++++++++----- offheap/object_reference_test.go | 8 ++--- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go index cf8bede..704d413 100644 --- a/offheap/internal/pointerstore/metadata.go +++ b/offheap/internal/pointerstore/metadata.go @@ -14,8 +14,8 @@ import "fmt" // value can access/free objects they point to. This is a best-effort safety // check to try to catch use-after-free type errors. type metadata struct { - nextFree RefPointer dataAddressAndGen taggedAddress + isFree bool } //gcassert:noescape diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index d836c5d..aadb27b 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -51,22 +51,18 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { // _and_ verify the generation of the reference. meta := r.metadata() meta.dataAddressAndGen = r.dataAddressAndGen + // The value defaults to false, but we write it here for readability + meta.isFree = false return r } //gcassert:noescape -func (r *RefPointer) AllocFromFree() (nextFree RefPointer) { - // Grab the nextFree reference, and nil it for this metadata +func (r *RefPointer) allocFromFree() { meta := r.metadata() - nextFree = meta.nextFree - meta.nextFree = RefPointer{} - // If the nextFree pointer points back to this Reference, then there - // are no more freed slots available - if nextFree == *r { - nextFree = RefPointer{} - } + // This object is now allocated and is no long free + meta.isFree = false // Increment the generation for the allocation and set that generation in // the Metadata and Reference @@ -74,15 +70,13 @@ func (r *RefPointer) AllocFromFree() (nextFree RefPointer) { gen++ meta.setGen(gen) r.setGen(gen) - - return nextFree } //gcassert:noescape -func (r *RefPointer) Free(oldFree RefPointer) { +func (r *RefPointer) free() { meta := r.metadata() - if !meta.nextFree.IsNil() { + if meta.isFree { // NB: The odd-looking *r here actually prevents an allocation. // Fuller explanation found in DataPtr() panic(fmt.Errorf("attempted to Free freed allocation %v", *r)) @@ -90,11 +84,8 @@ func (r *RefPointer) Free(oldFree RefPointer) { meta.checkReference(r) - if oldFree.IsNil() { - meta.nextFree = *r - } else { - meta.nextFree = oldFree - } + // Mark the object as free + meta.isFree = true } //gcassert:noescape @@ -106,7 +97,7 @@ func (r *RefPointer) IsNil() bool { func (r *RefPointer) DataPtr() uintptr { meta := r.metadata() - if !meta.nextFree.IsNil() { + if meta.isFree { // NB: We make a copy of r here - otherwise the compiler // believes that r itself escapes to the heap (not strictly // wrong) and will allocate it to the heap, even if this path diff --git a/offheap/internal/pointerstore/pointer_store.go b/offheap/internal/pointerstore/pointer_store.go index 500fe96..3934440 100644 --- a/offheap/internal/pointerstore/pointer_store.go +++ b/offheap/internal/pointerstore/pointer_store.go @@ -32,7 +32,7 @@ type Store struct { // freeRWLock protects rootFree freeLock sync.Mutex - rootFree RefPointer + freeList []RefPointer // objectsLock protects objects // Allocating to an existing slab with a free slot only needs a read lock @@ -67,8 +67,9 @@ func (s *Store) Free(r RefPointer) { s.freeLock.Lock() defer s.freeLock.Unlock() - r.Free(s.rootFree) - s.rootFree = r + // Set up the reference to be 'free' + r.free() + s.freeList = append(s.freeList, r) s.frees.Add(1) } @@ -124,15 +125,19 @@ func (s *Store) allocFromFree() (RefPointer, bool) { defer s.freeLock.Unlock() // No free objects available - allocFromFree failed - if s.rootFree.IsNil() { + if len(s.freeList) == 0 { return RefPointer{}, false } - // Get pointer to the next available freed slot - alloc := s.rootFree - s.rootFree = alloc.AllocFromFree() + // pop the most recently freed reference off the free list + // TODO might be worth writing a small stack for this + r := s.freeList[len(s.freeList)-1] + s.freeList = s.freeList[:len(s.freeList)-1] - return alloc, true + // set up the reference to be 'allocated' + r.allocFromFree() + + return r, true } func (s *Store) allocFromOffset() RefPointer { diff --git a/offheap/object_reference_test.go b/offheap/object_reference_test.go index f25d617..04368d6 100644 --- a/offheap/object_reference_test.go +++ b/offheap/object_reference_test.go @@ -345,10 +345,10 @@ func Test_Object_CannotAllocateVeryBigStruct(t *testing.T) { // // This test should alert us if this problem ever reappears. // -// NB: In the future meta-data will likely be moved to a separate allocation -// space and some details described above will become out of date. The test -// will still be useful though. Zero sized types are a likely source of -// edge-case bugs for all eternity. +// NB: At time of update meta-data has been moved to a separate allocation +// space and some details described above are out of date. The test is still +// useful though. Zero sized types are a likely source of edge-case bugs for +// all eternity. func Test_Object_ZeroSizedType_FullSlab(t *testing.T) { os := New() defer func() { From 4728a18f72a180d69f95b222e909cf7d371a590f Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Mon, 3 Feb 2025 19:29:16 +1300 Subject: [PATCH 10/29] Implement stack for free list The pointer store tracks a list of free RefPointers in a slice. This was managed as a stack but using a slice directly. We have implemented a very small stack implementation and use that instead. A little bit cleaner. --- offheap/internal/pointerstore/free_list.go | 24 +++++ .../internal/pointerstore/free_list_test.go | 95 +++++++++++++++++++ .../internal/pointerstore/pointer_store.go | 14 +-- 3 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 offheap/internal/pointerstore/free_list.go create mode 100644 offheap/internal/pointerstore/free_list_test.go diff --git a/offheap/internal/pointerstore/free_list.go b/offheap/internal/pointerstore/free_list.go new file mode 100644 index 0000000..e6d8555 --- /dev/null +++ b/offheap/internal/pointerstore/free_list.go @@ -0,0 +1,24 @@ +// Copyright 2025 Francis Michael Stephens. All rights reserved. Use of this +// source code is governed by an MIT license that can be found in the LICENSE +// file. + +package pointerstore + +type freeStack struct { + free []RefPointer +} + +func (s *freeStack) push(r RefPointer) { + s.free = append(s.free, r) +} + +func (s *freeStack) pop() (r RefPointer, ok bool) { + l := len(s.free) + if l == 0 { + return RefPointer{}, false + } + + r = s.free[l-1] + s.free = s.free[:l-1] + return r, true +} diff --git a/offheap/internal/pointerstore/free_list_test.go b/offheap/internal/pointerstore/free_list_test.go new file mode 100644 index 0000000..ffd35af --- /dev/null +++ b/offheap/internal/pointerstore/free_list_test.go @@ -0,0 +1,95 @@ +// Copyright 2025 Francis Michael Stephens. All rights reserved. Use of this +// source code is governed by an MIT license that can be found in the LICENSE +// file. + +package pointerstore + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFreeStack_Empty(t *testing.T) { + s := freeStack{} + r, ok := s.pop() + assert.True(t, r.IsNil()) + assert.False(t, ok) +} + +// Push 3 references onto the stack. Pop them off in reverse order +func TestFreeStack_PushPop(t *testing.T) { + allocConfig := NewAllocConfigBySize(8, 32*8) + objects, metadatas := MmapSlab(allocConfig) + + s := freeStack{} + + r1Push := NewReference(objects[0], metadatas[0]) + r2Push := NewReference(objects[1], metadatas[1]) + r3Push := NewReference(objects[2], metadatas[2]) + + s.push(r1Push) + s.push(r2Push) + s.push(r3Push) + + r3Pop, ok3 := s.pop() + assert.Equal(t, r3Push, r3Pop) + assert.True(t, ok3) + + r2Pop, ok2 := s.pop() + assert.Equal(t, r2Push, r2Pop) + assert.True(t, ok2) + + r1Pop, ok1 := s.pop() + assert.Equal(t, r1Push, r1Pop) + assert.True(t, ok1) + + r0Pop, ok0 := s.pop() + assert.True(t, r0Pop.IsNil()) + assert.False(t, ok0) +} + +// Push three references off, pop two, push two more. Pop remaining references. +func TestFreeStack_PushPopComplex(t *testing.T) { + allocConfig := NewAllocConfigBySize(8, 32*8) + objects, metadatas := MmapSlab(allocConfig) + + s := freeStack{} + + r1Push := NewReference(objects[0], metadatas[0]) + r2Push := NewReference(objects[1], metadatas[1]) + r3Push := NewReference(objects[2], metadatas[2]) + r4Push := NewReference(objects[3], metadatas[3]) + r5Push := NewReference(objects[4], metadatas[4]) + + s.push(r1Push) + s.push(r2Push) + s.push(r3Push) + + r3Pop, ok3 := s.pop() + assert.Equal(t, r3Push, r3Pop) + assert.True(t, ok3) + + r2Pop, ok2 := s.pop() + assert.Equal(t, r2Push, r2Pop) + assert.True(t, ok2) + + s.push(r4Push) + s.push(r5Push) + + r5Pop, ok5 := s.pop() + assert.Equal(t, r5Push, r5Pop) + assert.True(t, ok5) + + r4Pop, ok4 := s.pop() + assert.Equal(t, r4Push, r4Pop) + assert.True(t, ok4) + + r1Pop, ok1 := s.pop() + assert.Equal(t, r1Push, r1Pop) + assert.True(t, ok1) + + r0Pop, ok0 := s.pop() + assert.True(t, r0Pop.IsNil()) + assert.False(t, ok0) +} diff --git a/offheap/internal/pointerstore/pointer_store.go b/offheap/internal/pointerstore/pointer_store.go index 3934440..2e39793 100644 --- a/offheap/internal/pointerstore/pointer_store.go +++ b/offheap/internal/pointerstore/pointer_store.go @@ -32,7 +32,7 @@ type Store struct { // freeRWLock protects rootFree freeLock sync.Mutex - freeList []RefPointer + freeList freeStack // objectsLock protects objects // Allocating to an existing slab with a free slot only needs a read lock @@ -69,7 +69,7 @@ func (s *Store) Free(r RefPointer) { // Set up the reference to be 'free' r.free() - s.freeList = append(s.freeList, r) + s.freeList.push(r) s.frees.Add(1) } @@ -124,19 +124,13 @@ func (s *Store) allocFromFree() (RefPointer, bool) { s.freeLock.Lock() defer s.freeLock.Unlock() - // No free objects available - allocFromFree failed - if len(s.freeList) == 0 { + r, ok := s.freeList.pop() + if !ok { return RefPointer{}, false } - // pop the most recently freed reference off the free list - // TODO might be worth writing a small stack for this - r := s.freeList[len(s.freeList)-1] - s.freeList = s.freeList[:len(s.freeList)-1] - // set up the reference to be 'allocated' r.allocFromFree() - return r, true } From 13efba93bec8a7f666f5c55d24b8cfccf7388fc0 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:16:53 +1300 Subject: [PATCH 11/29] Incrementing generation tag on free and realloc We also add unit tests to demonstrate the expected reference and metadata behaviour for the 'free()' and 'allocFromFree()'. This is a unit testing hole which hand now been filled. --- .../pointerstore/pointer_reference.go | 64 ++++++++++--------- .../pointerstore/pointer_reference_test.go | 60 +++++++++++++++++ 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index aadb27b..59b1399 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -58,34 +58,55 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { } //gcassert:noescape -func (r *RefPointer) allocFromFree() { +func (r *RefPointer) free() { meta := r.metadata() - // This object is now allocated and is no long free - meta.isFree = false + if meta.isFree { + // NB: The odd-looking *r here actually prevents an allocation. + // Fuller explanation found in DataPtr() + panic(fmt.Errorf("attempted to Free freed allocation %v", *r)) + } + + meta.checkReference(r) + + // Mark the object as free + meta.isFree = true // Increment the generation for the allocation and set that generation in // the Metadata and Reference gen := meta.gen() gen++ meta.setGen(gen) - r.setGen(gen) } +// NB: In the future this method should return a new RefPointer with updated +// generation tag instead of modifying the method receiver. +// //gcassert:noescape -func (r *RefPointer) free() { +func (r *RefPointer) allocFromFree() { meta := r.metadata() - if meta.isFree { - // NB: The odd-looking *r here actually prevents an allocation. - // Fuller explanation found in DataPtr() - panic(fmt.Errorf("attempted to Free freed allocation %v", *r)) - } + // This object is now allocated and is no long free + meta.isFree = false + // Update this reference so it's generatation tag matches the metadata + r.setGen(meta.gen()) +} - meta.checkReference(r) +// This method re-allocates the memory location. When this method returns r +// will no longer be a valid reference. The reference returned _will_ be a +// valid reference to the same location. +func (r *RefPointer) Realloc() RefPointer { + newRef := *r - // Mark the object as free - meta.isFree = true + // Get metadata generation tag and increment it + meta := r.metadata() + gen := meta.gen() + gen++ + + // Set the new generation tag in both the metadata and reference + meta.setGen(gen) + newRef.setGen(gen) + return newRef } //gcassert:noescape @@ -139,20 +160,3 @@ func (r *RefPointer) Gen() uint8 { func (r *RefPointer) setGen(gen uint8) { r.dataAddressAndGen = r.dataAddressAndGen.withGen(gen) } - -// This method re-allocates the memory location. When this method returns r -// will no longer be a valid reference. The reference returned _will_ be a -// valid reference to the same location. -func (r *RefPointer) Realloc() RefPointer { - newRef := *r - - // Get metadata generation tag and increment it - meta := r.metadata() - gen := meta.gen() - gen++ - - // Set the new generation tag in both the metadata and reference - meta.setGen(gen) - newRef.setGen(gen) - return newRef -} diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index e14d4b7..cc2b5c9 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -94,6 +94,66 @@ func TestGenerationDoesNotAppearInOtherFields(t *testing.T) { assert.Equal(t, gen, r.Gen()) } +func TestFree(t *testing.T) { + allocConfig := NewAllocConfigBySize(8, 32*8) + objects, metadatas := MmapSlab(allocConfig) + + r := NewReference(objects[0], metadatas[0]) + meta := r.metadata() + + assert.False(t, meta.isFree) + assert.Equal(t, uint8(0), meta.dataAddressAndGen.gen()) + assert.Equal(t, uint8(0), r.Gen()) + + r.free() + + // The reference still points to the same metadata location + assert.Equal(t, meta, r.metadata()) + + // The metadata is now marked as free + assert.True(t, meta.isFree) + // After free is called the metadata for this reference has a new + // generation tag, while the reference has the same old generation tag + assert.Equal(t, uint8(1), meta.dataAddressAndGen.gen()) + assert.Equal(t, uint8(0), r.Gen()) + + // Accessng the data of a freed reference will panic + assert.Panics(t, func() { r.DataPtr() }) + + // Freeing a freed reference will panic + assert.Panics(t, func() { r.free() }) +} + +func TestAllocFromFree(t *testing.T) { + allocConfig := NewAllocConfigBySize(8, 32*8) + objects, metadatas := MmapSlab(allocConfig) + + r := NewReference(objects[0], metadatas[0]) + meta := r.metadata() + + assert.False(t, meta.isFree) + assert.Equal(t, uint8(0), meta.dataAddressAndGen.gen()) + assert.Equal(t, uint8(0), r.Gen()) + + r.free() + r.allocFromFree() + + // The reference still points to the same metadata location + assert.Equal(t, meta, r.metadata()) + + // The metadata is now marked as not free + assert.False(t, meta.isFree) + // After allocFromFree is called the reference's generation will match the metadata's generation + assert.Equal(t, uint8(1), meta.dataAddressAndGen.gen()) + assert.Equal(t, uint8(1), r.Gen()) + + // Accessng the data of an allocated-from-free reference will not panic + assert.NotPanics(t, func() { r.DataPtr() }) + + // Freeing an allocated-from-free reference will not panic + assert.NotPanics(t, func() { r.free() }) +} + func TestRealloc(t *testing.T) { allocConfig := NewAllocConfigBySize(8, 32*8) objects, metadatas := MmapSlab(allocConfig) From 6a65b779d3e19bb195788606008b2fb5607a364d Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:21:25 +1300 Subject: [PATCH 12/29] Add isFree bit to taggedAddress This shrinks the size of the generation tag to 7 bits. I think this is likely fine in practice. The new maximum generation size is 127 or 0x7F in hexadecimal. You can set and read the is-free bit with specific methods on taggedAddress. This is not currently used. Will be actually used in the next commit. --- .../pointerstore/pointer_reference_test.go | 7 ++-- .../internal/pointerstore/tagged_address.go | 34 +++++++++++++---- .../pointerstore/tagged_address_test.go | 38 ++++++++++++++++--- 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index cc2b5c9..a7c9441 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -85,13 +85,12 @@ func TestGenerationDoesNotAppearInOtherFields(t *testing.T) { metaPtr := r.metadataPtr() metadata := r.metadata() - gen := uint8(255) - metadata.setGen(gen) - r.setGen(gen) + metadata.setGen(maxGen) + r.setGen(maxGen) assert.Equal(t, dataPtr, r.DataPtr()) assert.Equal(t, metaPtr, r.metadataPtr()) - assert.Equal(t, gen, r.Gen()) + assert.Equal(t, uint8(maxGen), r.Gen()) } func TestFree(t *testing.T) { diff --git a/offheap/internal/pointerstore/tagged_address.go b/offheap/internal/pointerstore/tagged_address.go index 8e52be9..8302fb8 100644 --- a/offheap/internal/pointerstore/tagged_address.go +++ b/offheap/internal/pointerstore/tagged_address.go @@ -9,9 +9,17 @@ import ( "unsafe" ) -const maskShift = 56 // This leaves 8 bits for the generation data -const genMask = taggedAddress(0xFF << maskShift) -const pointerMask = ^genMask +const nilPtr = uintptr(0) +const maxGen = 0x7F + +const maskShift = 56 // This leaves 8 bits for the generation and isFree tags +const isFreeMask = taggedAddress(0x80 << maskShift) // Highest bit indicates if address is free +const genMask = taggedAddress(0x7F << maskShift) // Next 7 bits indicate generation tag +const tagMask = isFreeMask | genMask +const pointerMask = ^tagMask + +const setFree = taggedAddress(0x80 << maskShift) +const setNotFree = ^setFree type taggedAddress uint64 @@ -31,14 +39,26 @@ func (a taggedAddress) address() uintptr { return uintptr(a & pointerMask) } +func (a taggedAddress) bytes(size int) []byte { + return ([]byte)(unsafe.Slice((*byte)((unsafe.Pointer)(a.address())), size)) +} + +func (a taggedAddress) isFree() bool { + return a&setFree == setFree +} + +func (a taggedAddress) isNil() bool { + return a.address() == nilPtr +} + func (a taggedAddress) withGen(gen uint8) taggedAddress { return (a & pointerMask) | (taggedAddress(gen) << maskShift) } -func (a taggedAddress) bytes(size int) []byte { - return ([]byte)(unsafe.Slice((*byte)((unsafe.Pointer)(a.address())), size)) +func (a taggedAddress) withFree() taggedAddress { + return a | setFree } -func (a taggedAddress) isNil() bool { - return a.address() == 0 +func (a taggedAddress) withNotFree() taggedAddress { + return a & setNotFree } diff --git a/offheap/internal/pointerstore/tagged_address_test.go b/offheap/internal/pointerstore/tagged_address_test.go index b079eee..09f0e72 100644 --- a/offheap/internal/pointerstore/tagged_address_test.go +++ b/offheap/internal/pointerstore/tagged_address_test.go @@ -12,8 +12,7 @@ import ( "github.com/stretchr/testify/assert" ) -const addressShift = 64 - maskShift -const maxAddress = (1 << addressShift) - 1 +const maxAddress = (1 << (64 - maskShift)) - 1 // For a nil uintptr the address and generation tag are zero func TestTaggedAddress_ZeroValue(t *testing.T) { @@ -43,13 +42,13 @@ func TestTaggedAddress_AddressAndGenAreSeparate(t *testing.T) { newPtr := uintptr(rand.Int63n(maxAddress + 1)) ta := newTaggedAddress(newPtr) - for i := 1; i <= 255; i++ { + for i := 1; i <= maxGen; i++ { gen := uint8(i) // set tagged address generation ta = ta.withGen(gen) // If the address is not 0 then isNil() must be false - assert.Equal(t, newPtr == 0, ta.isNil()) + assert.Equal(t, newPtr == nilPtr, ta.isNil()) // Assert that the original address is preserved assert.Equal(t, newPtr, ta.address()) // Assert that the generation is preserved @@ -64,8 +63,7 @@ func TestTaggedAddress_PointerWithGenBitsPanics(t *testing.T) { for range 10 { newPtr := uintptr(rand.Int63n(maxAddress + 1)) - //for genBits := uintptr(1); genBits <= uintptr(255); genBits++ { - for i := 1; i <= 255; i++ { + for i := 1; i <= maxGen; i++ { genBits := uintptr(i) // Create a pointer with bits set in the gen tag bits badPtr := newPtr | ((genBits) << maskShift) @@ -73,3 +71,31 @@ func TestTaggedAddress_PointerWithGenBitsPanics(t *testing.T) { } } } + +func TestTaggedAddress_IsFree(t *testing.T) { + for range 10 { + newPtr := uintptr(rand.Int63n(maxAddress + 1)) + + ta := newTaggedAddress(newPtr) + address := ta.address() + gen := ta.gen() + // A tagged address is created not-free. It is assumed that a + // tagged address is only created for an allocation + assert.False(t, ta.isFree()) + // Neither the address nor the generation tag are changed by the is-free bit + assert.Equal(t, address, ta.address()) + assert.Equal(t, gen, ta.gen()) + + ta = ta.withFree() + assert.True(t, ta.isFree()) + // Neither the address nor the generation tag are changed by the is-free bit + assert.Equal(t, address, ta.address()) + assert.Equal(t, gen, ta.gen()) + + ta = ta.withNotFree() + assert.False(t, ta.isFree()) + // Neither the address nor the generation tag are changed by the is-free bit + assert.Equal(t, address, ta.address()) + assert.Equal(t, gen, ta.gen()) + } +} From 9263522638ed43885ebf40f76ca240a8e573b327 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:22:16 +1300 Subject: [PATCH 13/29] metadata now uses is-free bit in taggedAddress We remove the 'isFree' field as it is no longer needed. Fixed a big in taggedAddress.setGen(...), this was a sneaky one, as the pointerMask being used was unintentionally clearing the is-free bit. --- offheap/internal/pointerstore/metadata.go | 16 +++++++++++++++- .../internal/pointerstore/pointer_reference.go | 10 +++++----- .../pointerstore/pointer_reference_test.go | 8 ++++---- offheap/internal/pointerstore/tagged_address.go | 11 +++++------ .../internal/pointerstore/tagged_address_test.go | 11 +++++++++++ 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go index 704d413..067965d 100644 --- a/offheap/internal/pointerstore/metadata.go +++ b/offheap/internal/pointerstore/metadata.go @@ -15,7 +15,6 @@ import "fmt" // check to try to catch use-after-free type errors. type metadata struct { dataAddressAndGen taggedAddress - isFree bool } //gcassert:noescape @@ -28,6 +27,21 @@ func (m *metadata) setGen(gen uint8) { m.dataAddressAndGen = m.dataAddressAndGen.withGen(gen) } +//gcassert:noescape +func (m *metadata) isFree() bool { + return m.dataAddressAndGen.isFree() +} + +//gcassert:noescape +func (m *metadata) setFree() { + m.dataAddressAndGen = m.dataAddressAndGen.withFree() +} + +//gcassert:noescape +func (m *metadata) setNotFree() { + m.dataAddressAndGen = m.dataAddressAndGen.withNotFree() +} + // Check that the metadata for a reference agrees with the generation tag and the data address. // Failure results in a panic. // diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 59b1399..f923cef 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -52,7 +52,7 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { meta := r.metadata() meta.dataAddressAndGen = r.dataAddressAndGen // The value defaults to false, but we write it here for readability - meta.isFree = false + meta.setNotFree() return r } @@ -61,7 +61,7 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { func (r *RefPointer) free() { meta := r.metadata() - if meta.isFree { + if meta.isFree() { // NB: The odd-looking *r here actually prevents an allocation. // Fuller explanation found in DataPtr() panic(fmt.Errorf("attempted to Free freed allocation %v", *r)) @@ -70,7 +70,7 @@ func (r *RefPointer) free() { meta.checkReference(r) // Mark the object as free - meta.isFree = true + meta.setFree() // Increment the generation for the allocation and set that generation in // the Metadata and Reference @@ -87,7 +87,7 @@ func (r *RefPointer) allocFromFree() { meta := r.metadata() // This object is now allocated and is no long free - meta.isFree = false + meta.setNotFree() // Update this reference so it's generatation tag matches the metadata r.setGen(meta.gen()) } @@ -118,7 +118,7 @@ func (r *RefPointer) IsNil() bool { func (r *RefPointer) DataPtr() uintptr { meta := r.metadata() - if meta.isFree { + if meta.isFree() { // NB: We make a copy of r here - otherwise the compiler // believes that r itself escapes to the heap (not strictly // wrong) and will allocate it to the heap, even if this path diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index a7c9441..fc3a355 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -100,7 +100,7 @@ func TestFree(t *testing.T) { r := NewReference(objects[0], metadatas[0]) meta := r.metadata() - assert.False(t, meta.isFree) + assert.False(t, meta.isFree()) assert.Equal(t, uint8(0), meta.dataAddressAndGen.gen()) assert.Equal(t, uint8(0), r.Gen()) @@ -110,7 +110,7 @@ func TestFree(t *testing.T) { assert.Equal(t, meta, r.metadata()) // The metadata is now marked as free - assert.True(t, meta.isFree) + assert.True(t, meta.isFree()) // After free is called the metadata for this reference has a new // generation tag, while the reference has the same old generation tag assert.Equal(t, uint8(1), meta.dataAddressAndGen.gen()) @@ -130,7 +130,7 @@ func TestAllocFromFree(t *testing.T) { r := NewReference(objects[0], metadatas[0]) meta := r.metadata() - assert.False(t, meta.isFree) + assert.False(t, meta.isFree()) assert.Equal(t, uint8(0), meta.dataAddressAndGen.gen()) assert.Equal(t, uint8(0), r.Gen()) @@ -141,7 +141,7 @@ func TestAllocFromFree(t *testing.T) { assert.Equal(t, meta, r.metadata()) // The metadata is now marked as not free - assert.False(t, meta.isFree) + assert.False(t, meta.isFree()) // After allocFromFree is called the reference's generation will match the metadata's generation assert.Equal(t, uint8(1), meta.dataAddressAndGen.gen()) assert.Equal(t, uint8(1), r.Gen()) diff --git a/offheap/internal/pointerstore/tagged_address.go b/offheap/internal/pointerstore/tagged_address.go index 8302fb8..c0d385e 100644 --- a/offheap/internal/pointerstore/tagged_address.go +++ b/offheap/internal/pointerstore/tagged_address.go @@ -11,14 +11,13 @@ import ( const nilPtr = uintptr(0) const maxGen = 0x7F - const maskShift = 56 // This leaves 8 bits for the generation and isFree tags const isFreeMask = taggedAddress(0x80 << maskShift) // Highest bit indicates if address is free -const genMask = taggedAddress(0x7F << maskShift) // Next 7 bits indicate generation tag -const tagMask = isFreeMask | genMask -const pointerMask = ^tagMask +const genMask = taggedAddress(maxGen << maskShift) // Next 7 bits indicate generation tag +const tagMask = isFreeMask | genMask // Mask revealing all 8 tag bits +const pointerMask = ^tagMask // Mask revealing 56 address bits -const setFree = taggedAddress(0x80 << maskShift) +const setFree = isFreeMask const setNotFree = ^setFree type taggedAddress uint64 @@ -52,7 +51,7 @@ func (a taggedAddress) isNil() bool { } func (a taggedAddress) withGen(gen uint8) taggedAddress { - return (a & pointerMask) | (taggedAddress(gen) << maskShift) + return (a & (pointerMask | isFreeMask)) | (taggedAddress(gen) << maskShift) } func (a taggedAddress) withFree() taggedAddress { diff --git a/offheap/internal/pointerstore/tagged_address_test.go b/offheap/internal/pointerstore/tagged_address_test.go index 09f0e72..546827f 100644 --- a/offheap/internal/pointerstore/tagged_address_test.go +++ b/offheap/internal/pointerstore/tagged_address_test.go @@ -92,6 +92,17 @@ func TestTaggedAddress_IsFree(t *testing.T) { assert.Equal(t, address, ta.address()) assert.Equal(t, gen, ta.gen()) + // Side-quest, set the generation tag and ensure this doesn't + // interfere with the is-free bit + ta = ta.withGen(maxGen) + assert.True(t, ta.isFree()) + // Neither the address nor the generation tag are changed by the is-free bit + assert.Equal(t, address, ta.address()) + assert.Equal(t, uint8(maxGen), ta.gen()) + + // Set the generation back + ta = ta.withGen(gen) + ta = ta.withNotFree() assert.False(t, ta.isFree()) // Neither the address nor the generation tag are changed by the is-free bit From efa96fd1c09accadf927f53bdeb50daa03d7f8fa Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:22:37 +1300 Subject: [PATCH 14/29] tidy-up of the taggedAddress constants Move comments around to improve readability, and rename pointer* constants to address* constants. --- .../internal/pointerstore/tagged_address.go | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/offheap/internal/pointerstore/tagged_address.go b/offheap/internal/pointerstore/tagged_address.go index c0d385e..d8329f9 100644 --- a/offheap/internal/pointerstore/tagged_address.go +++ b/offheap/internal/pointerstore/tagged_address.go @@ -9,16 +9,28 @@ import ( "unsafe" ) -const nilPtr = uintptr(0) -const maxGen = 0x7F -const maskShift = 56 // This leaves 8 bits for the generation and isFree tags -const isFreeMask = taggedAddress(0x80 << maskShift) // Highest bit indicates if address is free -const genMask = taggedAddress(maxGen << maskShift) // Next 7 bits indicate generation tag -const tagMask = isFreeMask | genMask // Mask revealing all 8 tag bits -const pointerMask = ^tagMask // Mask revealing 56 address bits - -const setFree = isFreeMask -const setNotFree = ^setFree +const ( + // handy nil pointer constant + nilPtr = uintptr(0) + // maximum value of the generation tag (127) + maxGen = 0x7F + // This leaves 8 bits for the generation and isFree tags + maskShift = 56 + // Highest bit indicates if address is free + isFreeMask = taggedAddress(0x80 << maskShift) + // Next 7 bits indicate generation tag + genMask = taggedAddress(maxGen << maskShift) + // Mask revealing all 8 tag bits + tagMask = isFreeMask | genMask + // Mask revealing 56 address bits + addressMask = ^tagMask + // Mask revealing address and is-free bit, allows us to safely set generation tag + addressAndIsFreeMask = addressMask | isFreeMask + // Mask which can be used to directly set the is-free bit to 1 + setFree = isFreeMask + // Mask which can be used to directly set the is-free bit to 0 + setNotFree = ^setFree +) type taggedAddress uint64 @@ -35,7 +47,7 @@ func (a taggedAddress) gen() uint8 { } func (a taggedAddress) address() uintptr { - return uintptr(a & pointerMask) + return uintptr(a & addressMask) } func (a taggedAddress) bytes(size int) []byte { @@ -51,7 +63,7 @@ func (a taggedAddress) isNil() bool { } func (a taggedAddress) withGen(gen uint8) taggedAddress { - return (a & (pointerMask | isFreeMask)) | (taggedAddress(gen) << maskShift) + return (a & (addressAndIsFreeMask)) | (taggedAddress(gen) << maskShift) } func (a taggedAddress) withFree() taggedAddress { From de910e490e5070366cde91daa5116f06e99e99de Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:22:55 +1300 Subject: [PATCH 15/29] Add comment to checkReference() method This simply clarifies that we don't check the is-free bit in this method because it is never set in any RefPointers. --- offheap/internal/pointerstore/metadata.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go index 067965d..5bc37b1 100644 --- a/offheap/internal/pointerstore/metadata.go +++ b/offheap/internal/pointerstore/metadata.go @@ -42,8 +42,11 @@ func (m *metadata) setNotFree() { m.dataAddressAndGen = m.dataAddressAndGen.withNotFree() } -// Check that the metadata for a reference agrees with the generation tag and the data address. -// Failure results in a panic. +// Check that the metadata for a reference agrees with the generation tag and +// the data address. Failure results in a panic. +// +// Please note, we never set the is-free bit in the taggedAddress of the +// RefPointer. So we don't expect them to match and don't check that here. // //gcassert:noescape func (m *metadata) checkReference(r *RefPointer) { From a884022d4ed31977bfa0ba7d61c5c20fc62d741f Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:25:05 +1300 Subject: [PATCH 16/29] Renamed dataAddressAndGen to address in RefPointer Next step will this field will become the _only_ address and it will point to metadata only. The metadata will contain the actual data address. --- offheap/internal/pointerstore/metadata.go | 6 +++--- .../pointerstore/pointer_reference.go | 20 +++++++++---------- .../pointerstore/pointer_reference_test.go | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go index 5bc37b1..ff010a6 100644 --- a/offheap/internal/pointerstore/metadata.go +++ b/offheap/internal/pointerstore/metadata.go @@ -50,14 +50,14 @@ func (m *metadata) setNotFree() { // //gcassert:noescape func (m *metadata) checkReference(r *RefPointer) { - if m.dataAddressAndGen != r.dataAddressAndGen { + if m.dataAddressAndGen != r.address { mGen := m.gen() rGen := r.Gen() mAddress := m.dataAddressAndGen.address() - rAddress := r.dataAddressAndGen.address() + rAddress := r.address.address() genMismatchMessage := fmt.Sprintf("generation mismatch between metadata (%d) and reference (%d)", m.gen(), r.Gen()) - addressMismatchMessage := fmt.Sprintf("address mismatch between metadata (%#0*X) and reference (%#0*X)", 14, m.dataAddressAndGen.address(), 14, r.dataAddressAndGen.address()) + addressMismatchMessage := fmt.Sprintf("address mismatch between metadata (%#0*X) and reference (%#0*X)", 14, m.dataAddressAndGen.address(), 14, r.address.address()) switch { case mGen != rGen && mAddress != rAddress: diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index f923cef..afb412b 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -27,8 +27,8 @@ import ( // is a meaningful improvement are speculative and haven't been tested. This // would be a good target for future performance testing. type RefPointer struct { - dataAddressAndGen taggedAddress - metaAddress taggedAddress + address taggedAddress + metaAddress taggedAddress } func NewReference(dataAddress, metaAddress uintptr) RefPointer { @@ -41,8 +41,8 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { } r := RefPointer{ - dataAddressAndGen: newTaggedAddress(dataAddress), - metaAddress: newTaggedAddress(metaAddress), + address: newTaggedAddress(dataAddress), + metaAddress: newTaggedAddress(metaAddress), } // Set the dataAddressAndGen in this reference's metadata. @@ -50,7 +50,7 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { // In one of the next steps we will use this to look up the actual data // _and_ verify the generation of the reference. meta := r.metadata() - meta.dataAddressAndGen = r.dataAddressAndGen + meta.dataAddressAndGen = r.address // The value defaults to false, but we write it here for readability meta.setNotFree() @@ -111,7 +111,7 @@ func (r *RefPointer) Realloc() RefPointer { //gcassert:noescape func (r *RefPointer) IsNil() bool { - return r.dataAddressAndGen.isNil() + return r.address.isNil() } //gcassert:noescape @@ -131,14 +131,14 @@ func (r *RefPointer) DataPtr() uintptr { meta.checkReference(r) - return r.dataAddressAndGen.address() + return r.address.address() } // Convenient method to retrieve raw data of an allocation // //gcassert:noescape func (r *RefPointer) Bytes(size int) []byte { - return r.dataAddressAndGen.bytes(size) + return r.address.bytes(size) } //gcassert:noescape @@ -153,10 +153,10 @@ func (r *RefPointer) metadata() *metadata { //gcassert:noescape func (r *RefPointer) Gen() uint8 { - return r.dataAddressAndGen.gen() + return r.address.gen() } //gcassert:noescape func (r *RefPointer) setGen(gen uint8) { - r.dataAddressAndGen = r.dataAddressAndGen.withGen(gen) + r.address = r.address.withGen(gen) } diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index fc3a355..fbd0029 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -58,7 +58,7 @@ func TestNewReference(t *testing.T) { assert.Equal(t, uint8(0), r.Gen()) // The dataAddressAndGen matches in both the reference and the metadata meta := r.metadata() - assert.Equal(t, r.dataAddressAndGen, meta.dataAddressAndGen) + assert.Equal(t, r.address, meta.dataAddressAndGen) } } @@ -168,9 +168,9 @@ func TestRealloc(t *testing.T) { meta2 := r2.metadata() // The dataAddressAndGen matches in both the reference and the metadata - assert.Equal(t, r2.dataAddressAndGen, meta2.dataAddressAndGen) + assert.Equal(t, r2.address, meta2.dataAddressAndGen) // The dataAddressAndGen of the original metadata has been updated to match r2 - assert.Equal(t, r2.dataAddressAndGen, meta1.dataAddressAndGen) + assert.Equal(t, r2.address, meta1.dataAddressAndGen) // Assert that the data/metadata pointed to by r1 and r2 is the same assert.Equal(t, dataPtr, r2.DataPtr()) From 1e5bd8b1a92601b232e76ed7016a12f9f4482f5c Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:28:16 +1300 Subject: [PATCH 17/29] Fix metadata checks for RefPointer.Bytes(size) This method was bypassing the meta-data checks for data access. We create a common method called by both 'Bytes()' and 'DataPtr()', which does these checks. --- .../pointerstore/pointer_reference.go | 20 +++++++++++-------- .../pointerstore/pointer_reference_test.go | 10 +++++++++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index afb412b..0659dcd 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -116,6 +116,17 @@ func (r *RefPointer) IsNil() bool { //gcassert:noescape func (r *RefPointer) DataPtr() uintptr { + return r.dataAddress().address() +} + +// Convenient method to retrieve raw data of an allocation +// +//gcassert:noescape +func (r *RefPointer) Bytes(size int) []byte { + return r.dataAddress().bytes(size) +} + +func (r *RefPointer) dataAddress() taggedAddress { meta := r.metadata() if meta.isFree() { @@ -131,14 +142,7 @@ func (r *RefPointer) DataPtr() uintptr { meta.checkReference(r) - return r.address.address() -} - -// Convenient method to retrieve raw data of an allocation -// -//gcassert:noescape -func (r *RefPointer) Bytes(size int) []byte { - return r.address.bytes(size) + return r.address } //gcassert:noescape diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index fbd0029..3924abe 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -52,6 +52,8 @@ func TestNewReference(t *testing.T) { assert.False(t, r.IsNil()) // Data pointer points to the correct location assert.Equal(t, objects[i], r.DataPtr()) + // Bytes points to data at the correct location + assert.Equal(t, objects[i], uintptr(unsafe.Pointer(&r.Bytes(8)[0]))) // Metadata pointer points to the correct location assert.Equal(t, metadata[i], r.metadataPtr()) // Generation of a new Reference is always 0 @@ -118,6 +120,7 @@ func TestFree(t *testing.T) { // Accessng the data of a freed reference will panic assert.Panics(t, func() { r.DataPtr() }) + assert.Panics(t, func() { r.Bytes(8) }) // Freeing a freed reference will panic assert.Panics(t, func() { r.free() }) @@ -148,6 +151,7 @@ func TestAllocFromFree(t *testing.T) { // Accessng the data of an allocated-from-free reference will not panic assert.NotPanics(t, func() { r.DataPtr() }) + assert.NotPanics(t, func() { r.Bytes(8) }) // Freeing an allocated-from-free reference will not panic assert.NotPanics(t, func() { r.free() }) @@ -179,7 +183,11 @@ func TestRealloc(t *testing.T) { // Assert that r2 has a different generation than r1 assert.NotEqual(t, gen, r2.Gen()) - // Assert that r1 is no longer valid, but r2 is valid + // Assert that data is no longer accessible through r1 assert.Panics(t, func() { r1.DataPtr() }) + assert.Panics(t, func() { r1.Bytes(8) }) + + // Assert that data is accessible through r2 assert.NotPanics(t, func() { r2.DataPtr() }) + assert.NotPanics(t, func() { r2.Bytes(8) }) } From 14d2b2f44906bef6447cf84426391e84684fe41e Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:27:44 +1300 Subject: [PATCH 18/29] RefPointer _only_ contains address of metadata The metadata now contains the address of the actual data. This means that the RefPointer is only 64 bits wide. This also means that right now you must do two dependent memory lookups to get to the data. This is _probably_ slower than previously where we also did two memory lookups but they were independent and speculative execuation _probably_ initiates the second lookup earlier. This slow down is ok (and only theoretical, not benchmarked or conclusively tested). Making the RefPointer only 64 bits wide allows us to do vanilla atomic operations like CAS etc. Previously doing CAS on two words looked potentially very complex and likely much slower. We will introduce a const flag in the future which can be switched off at build time to bypass the metadata lookup and store the data address directly in the RefPointer. This will speed up data access times, but only when the flag is switched off at compile time. A good idea? A bad idea? Ony time will tell. (It's for sure an idea) --- offheap/internal/pointerstore/metadata.go | 25 +++------------- .../pointerstore/pointer_reference.go | 30 +++++++++++++------ .../pointerstore/pointer_reference_test.go | 30 ++++++++++--------- .../pointerstore/pointer_store_test.go | 4 +-- .../internal/pointerstore/tagged_address.go | 2 +- 5 files changed, 44 insertions(+), 47 deletions(-) diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go index ff010a6..73e0008 100644 --- a/offheap/internal/pointerstore/metadata.go +++ b/offheap/internal/pointerstore/metadata.go @@ -42,32 +42,15 @@ func (m *metadata) setNotFree() { m.dataAddressAndGen = m.dataAddressAndGen.withNotFree() } -// Check that the metadata for a reference agrees with the generation tag and -// the data address. Failure results in a panic. +// Check that the metadata for a reference agrees with the generation tag. +// Failure results in a panic. // // Please note, we never set the is-free bit in the taggedAddress of the // RefPointer. So we don't expect them to match and don't check that here. // //gcassert:noescape func (m *metadata) checkReference(r *RefPointer) { - if m.dataAddressAndGen != r.address { - mGen := m.gen() - rGen := r.Gen() - mAddress := m.dataAddressAndGen.address() - rAddress := r.address.address() - - genMismatchMessage := fmt.Sprintf("generation mismatch between metadata (%d) and reference (%d)", m.gen(), r.Gen()) - addressMismatchMessage := fmt.Sprintf("address mismatch between metadata (%#0*X) and reference (%#0*X)", 14, m.dataAddressAndGen.address(), 14, r.address.address()) - - switch { - case mGen != rGen && mAddress != rAddress: - panic(fmt.Errorf(genMismatchMessage + " and " + addressMismatchMessage)) - - case mGen != rGen: - panic(fmt.Errorf(genMismatchMessage)) - - case mAddress != rAddress: - panic(fmt.Errorf(addressMismatchMessage)) - } + if m.gen() != r.Gen() { + panic(fmt.Errorf("generation mismatch between metadata (%d) and reference (%d)", m.gen(), r.Gen())) } } diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 0659dcd..7e8ff5c 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -27,8 +27,7 @@ import ( // is a meaningful improvement are speculative and haven't been tested. This // would be a good target for future performance testing. type RefPointer struct { - address taggedAddress - metaAddress taggedAddress + address taggedAddress } func NewReference(dataAddress, metaAddress uintptr) RefPointer { @@ -41,8 +40,7 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { } r := RefPointer{ - address: newTaggedAddress(dataAddress), - metaAddress: newTaggedAddress(metaAddress), + address: newTaggedAddress(metaAddress), } // Set the dataAddressAndGen in this reference's metadata. @@ -50,7 +48,7 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { // In one of the next steps we will use this to look up the actual data // _and_ verify the generation of the reference. meta := r.metadata() - meta.dataAddressAndGen = r.address + meta.dataAddressAndGen = newTaggedAddress(dataAddress) // The value defaults to false, but we write it here for readability meta.setNotFree() @@ -95,6 +93,8 @@ func (r *RefPointer) allocFromFree() { // This method re-allocates the memory location. When this method returns r // will no longer be a valid reference. The reference returned _will_ be a // valid reference to the same location. +// +//gcassert:noescape func (r *RefPointer) Realloc() RefPointer { newRef := *r @@ -126,6 +126,13 @@ func (r *RefPointer) Bytes(size int) []byte { return r.dataAddress().bytes(size) } +// Calling this method +// +// 1: looks up the metadata for this reference +// 2: Verifies that the reference is _allowed_ to access this data +// 3: Returns the taggedAddress pointing to the actual data +// +//gcassert:noescape func (r *RefPointer) dataAddress() taggedAddress { meta := r.metadata() @@ -142,17 +149,22 @@ func (r *RefPointer) dataAddress() taggedAddress { meta.checkReference(r) - return r.address + return meta.dataAddressAndGen } //gcassert:noescape -func (r *RefPointer) metadataPtr() uintptr { - return r.metaAddress.address() +func (r *RefPointer) metaPtr() uintptr { + return r.metaAddress().address() } //gcassert:noescape func (r *RefPointer) metadata() *metadata { - return (*metadata)(unsafe.Pointer(r.metadataPtr())) + return (*metadata)(unsafe.Pointer(r.metaAddress().address())) +} + +//gcassert:noescape +func (r *RefPointer) metaAddress() taggedAddress { + return r.address } //gcassert:noescape diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index 3924abe..b3dde48 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -55,12 +55,12 @@ func TestNewReference(t *testing.T) { // Bytes points to data at the correct location assert.Equal(t, objects[i], uintptr(unsafe.Pointer(&r.Bytes(8)[0]))) // Metadata pointer points to the correct location - assert.Equal(t, metadata[i], r.metadataPtr()) + assert.Equal(t, metadata[i], r.metaAddress().address()) // Generation of a new Reference is always 0 assert.Equal(t, uint8(0), r.Gen()) - // The dataAddressAndGen matches in both the reference and the metadata - meta := r.metadata() - assert.Equal(t, r.address, meta.dataAddressAndGen) + // The data should be accessible through this reference + assert.NotPanics(t, func() { r.DataPtr() }) + assert.NotPanics(t, func() { r.Bytes(8) }) } } @@ -84,14 +84,14 @@ func TestGenerationDoesNotAppearInOtherFields(t *testing.T) { r := NewReference(objects[0], metadatas[0]) dataPtr := r.DataPtr() - metaPtr := r.metadataPtr() + metaPtr := r.metaPtr() metadata := r.metadata() metadata.setGen(maxGen) r.setGen(maxGen) assert.Equal(t, dataPtr, r.DataPtr()) - assert.Equal(t, metaPtr, r.metadataPtr()) + assert.Equal(t, metaPtr, r.metaPtr()) assert.Equal(t, uint8(maxGen), r.Gen()) } @@ -165,29 +165,31 @@ func TestRealloc(t *testing.T) { meta1 := r1.metadata() dataPtr := r1.DataPtr() - metaPtr := r1.metadataPtr() + metaPtr := r1.metaPtr() gen := r1.Gen() r2 := r1.Realloc() meta2 := r2.metadata() - // The dataAddressAndGen matches in both the reference and the metadata - assert.Equal(t, r2.address, meta2.dataAddressAndGen) - // The dataAddressAndGen of the original metadata has been updated to match r2 - assert.Equal(t, r2.address, meta1.dataAddressAndGen) + // The metadata of r1 and r2 is the same location + assert.Equal(t, meta1, meta2) + // The generation matches in both the r2 and the metadata + assert.Equal(t, r2.Gen(), meta2.gen()) + // The generation does not match between r1 and the metadata + assert.NotEqual(t, r1.Gen(), meta2.gen()) // Assert that the data/metadata pointed to by r1 and r2 is the same assert.Equal(t, dataPtr, r2.DataPtr()) - assert.Equal(t, metaPtr, r2.metadataPtr()) + assert.Equal(t, metaPtr, r2.metaPtr()) // Assert that r2 has a different generation than r1 assert.NotEqual(t, gen, r2.Gen()) - // Assert that data is no longer accessible through r1 + // Assert that data is no longer accessible via r1 assert.Panics(t, func() { r1.DataPtr() }) assert.Panics(t, func() { r1.Bytes(8) }) - // Assert that data is accessible through r2 + // Assert that data is accessible via r2 assert.NotPanics(t, func() { r2.DataPtr() }) assert.NotPanics(t, func() { r2.Bytes(8) }) } diff --git a/offheap/internal/pointerstore/pointer_store_test.go b/offheap/internal/pointerstore/pointer_store_test.go index 1844db7..26b051c 100644 --- a/offheap/internal/pointerstore/pointer_store_test.go +++ b/offheap/internal/pointerstore/pointer_store_test.go @@ -73,7 +73,7 @@ func TestSlabIntegrity(t *testing.T) { } baseSlabData := refs[0].DataPtr() - baseSlabMetadata := refs[0].metadataPtr() + baseSlabMetadata := refs[0].metaPtr() // Check that the metadata is allocated immediately _after_ the data assert.Equal(t, baseSlabMetadata, baseSlabData+uintptr(conf.TotalObjectSize)) @@ -86,7 +86,7 @@ func TestSlabIntegrity(t *testing.T) { assert.Equal(t, baseSlabData+expectedDataOffset, dataPtr) // Check that the metadata allocations are spaced out appropriately - metaPtr := ref.metadataPtr() + metaPtr := ref.metaPtr() expectedMetaOffset := uintptr(conf.MetadataSize) * uintptr(i) assert.Equal(t, baseSlabMetadata+expectedMetaOffset, metaPtr) } diff --git a/offheap/internal/pointerstore/tagged_address.go b/offheap/internal/pointerstore/tagged_address.go index d8329f9..699ceca 100644 --- a/offheap/internal/pointerstore/tagged_address.go +++ b/offheap/internal/pointerstore/tagged_address.go @@ -37,7 +37,7 @@ type taggedAddress uint64 func newTaggedAddress(ptr uintptr) taggedAddress { ta := taggedAddress(ptr) if ta.gen() != 0 { - panic(fmt.Errorf("Cannot create tagged address from pointer (%d) with bits set in the generation tag (%d).", ptr, ta.gen())) + panic(fmt.Errorf("cannot create tagged address from pointer (%d) with bits set in the generation tag (%d)", ptr, ta.gen())) } return ta } From 4bfa75a1a261a1f517d1c3682a7ae039ed20f0de Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 18:56:39 +1300 Subject: [PATCH 19/29] Use nilPtr constant in RefPointer constructor --- offheap/internal/pointerstore/pointer_reference.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 7e8ff5c..7f22572 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -31,11 +31,11 @@ type RefPointer struct { } func NewReference(dataAddress, metaAddress uintptr) RefPointer { - if dataAddress == (uintptr)(unsafe.Pointer(nil)) { + if dataAddress == nilPtr { panic("cannot create new Reference with nil data pointer") } - if metaAddress == (uintptr)(unsafe.Pointer(nil)) { + if metaAddress == nilPtr { panic("cannot create new Reference with nil metadata pointer") } From cc3e6c374afd535de6995126a72c86a9a6b36060 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 19:00:49 +1300 Subject: [PATCH 20/29] Check RefPointer is valid on Realloc Previously we weren't checking this, so any old Reference could Realloc() an allocation at any time, even if that Reference was no longer valid. It's concerning that all this work has uncovered multiple holes in the Reference generation checking logic. It's not clear how to improve this beyond just thinking harder and reviewing code more carefully (neither of these approaches are very reliable ways to find bugs in general). But we will keep this problem in the back of our heads and see if we can improve things. --- offheap/internal/pointerstore/pointer_reference.go | 3 +++ offheap/internal/pointerstore/pointer_reference_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 7f22572..bdaa5c5 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -96,6 +96,9 @@ func (r *RefPointer) allocFromFree() { // //gcassert:noescape func (r *RefPointer) Realloc() RefPointer { + // Test that this reference is actually allowed to access the allocation + r.dataAddress() + newRef := *r // Get metadata generation tag and increment it diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index b3dde48..241914e 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -122,6 +122,9 @@ func TestFree(t *testing.T) { assert.Panics(t, func() { r.DataPtr() }) assert.Panics(t, func() { r.Bytes(8) }) + // Reallocing a freed reference will panic + assert.Panics(t, func() { r.Realloc() }) + // Freeing a freed reference will panic assert.Panics(t, func() { r.free() }) } From 214ed7fcb56c68a052cf60673111dbc6ce314bcb Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 19:18:28 +1300 Subject: [PATCH 21/29] Check if allocation is free in allocFromFree() It is not acceptable to call RefPointer.allocFromFree() when the allocation is not free. This hole is fixed and tested. --- offheap/internal/pointerstore/pointer_reference.go | 4 ++++ offheap/internal/pointerstore/pointer_reference_test.go | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index bdaa5c5..f8f228c 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -84,6 +84,10 @@ func (r *RefPointer) free() { func (r *RefPointer) allocFromFree() { meta := r.metadata() + if !meta.isFree() { + panic(fmt.Errorf("attempt to alloc-from-free active allocation %v", *r)) + } + // This object is now allocated and is no long free meta.setNotFree() // Update this reference so it's generatation tag matches the metadata diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index 241914e..b96eadc 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -160,6 +160,15 @@ func TestAllocFromFree(t *testing.T) { assert.NotPanics(t, func() { r.free() }) } +func TestAllocFromFree_NoFree(t *testing.T) { + allocConfig := NewAllocConfigBySize(8, 32*8) + objects, metadatas := MmapSlab(allocConfig) + + r := NewReference(objects[0], metadatas[0]) + + assert.Panics(t, func() { r.allocFromFree() }) +} + func TestRealloc(t *testing.T) { allocConfig := NewAllocConfigBySize(8, 32*8) objects, metadatas := MmapSlab(allocConfig) From 2a2fbf8076b3b2aa8a786681444bcbb93e238012 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 19:02:45 +1300 Subject: [PATCH 22/29] Rename dataAddress() -> accessibleActiveAddress() This RefPointer method does all the generation/is-free checks on an allocation. It's important that this method sits on the path of any RefPointer method which accesses the data or meta-data. Current methods which call this method are: Realloc() DataPtr() Bytes() free() Also inlined the metadata.checkReference() method. The check in metadata _only_ checked that the generation matches, which doesn't really warrant a separate method. The code seems more readable with the generation check inlined into the accessibleActiveAddress() method. --- offheap/internal/pointerstore/metadata.go | 15 ----------- .../pointerstore/pointer_reference.go | 26 +++++++++---------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go index 73e0008..9372938 100644 --- a/offheap/internal/pointerstore/metadata.go +++ b/offheap/internal/pointerstore/metadata.go @@ -4,8 +4,6 @@ package pointerstore -import "fmt" - // If the object's metadata has a non-nil nextFree pointer then the object is // currently free. Object's which have never been allocated are implicitly // free, but have a nil nextFree. @@ -41,16 +39,3 @@ func (m *metadata) setFree() { func (m *metadata) setNotFree() { m.dataAddressAndGen = m.dataAddressAndGen.withNotFree() } - -// Check that the metadata for a reference agrees with the generation tag. -// Failure results in a panic. -// -// Please note, we never set the is-free bit in the taggedAddress of the -// RefPointer. So we don't expect them to match and don't check that here. -// -//gcassert:noescape -func (m *metadata) checkReference(r *RefPointer) { - if m.gen() != r.Gen() { - panic(fmt.Errorf("generation mismatch between metadata (%d) and reference (%d)", m.gen(), r.Gen())) - } -} diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index f8f228c..b63e936 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -57,15 +57,10 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { //gcassert:noescape func (r *RefPointer) free() { - meta := r.metadata() - - if meta.isFree() { - // NB: The odd-looking *r here actually prevents an allocation. - // Fuller explanation found in DataPtr() - panic(fmt.Errorf("attempted to Free freed allocation %v", *r)) - } + // Check that this reference can access the allocation to free it + r.accessibleActiveAddress() - meta.checkReference(r) + meta := r.metadata() // Mark the object as free meta.setFree() @@ -90,6 +85,7 @@ func (r *RefPointer) allocFromFree() { // This object is now allocated and is no long free meta.setNotFree() + // Update this reference so it's generatation tag matches the metadata r.setGen(meta.gen()) } @@ -101,7 +97,7 @@ func (r *RefPointer) allocFromFree() { //gcassert:noescape func (r *RefPointer) Realloc() RefPointer { // Test that this reference is actually allowed to access the allocation - r.dataAddress() + r.accessibleActiveAddress() newRef := *r @@ -123,14 +119,14 @@ func (r *RefPointer) IsNil() bool { //gcassert:noescape func (r *RefPointer) DataPtr() uintptr { - return r.dataAddress().address() + return r.accessibleActiveAddress().address() } // Convenient method to retrieve raw data of an allocation // //gcassert:noescape func (r *RefPointer) Bytes(size int) []byte { - return r.dataAddress().bytes(size) + return r.accessibleActiveAddress().bytes(size) } // Calling this method @@ -140,7 +136,7 @@ func (r *RefPointer) Bytes(size int) []byte { // 3: Returns the taggedAddress pointing to the actual data // //gcassert:noescape -func (r *RefPointer) dataAddress() taggedAddress { +func (r *RefPointer) accessibleActiveAddress() taggedAddress { meta := r.metadata() if meta.isFree() { @@ -151,10 +147,12 @@ func (r *RefPointer) dataAddress() taggedAddress { // call, but if we don't take a copy of r in the fmt call, then // every call will allocate regardless of whether the method // panics or not - panic(fmt.Errorf("attempt to get freed allocation %v", *r)) + panic(fmt.Errorf("attempt to access freed allocation %v", *r)) } - meta.checkReference(r) + if meta.gen() != r.Gen() { + panic(fmt.Errorf("generation mismatch between metadata (%d) and reference (%d)", meta.gen(), r.Gen())) + } return meta.dataAddressAndGen } From fe5506d106301f908ebd6b39696a5ea419bff67a Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 19:37:13 +1300 Subject: [PATCH 23/29] RefPointer.allocFromFree() creates new RefPointer Previously the code would update the method receiver in place. Now we create a new RefPointer with the correct generation for the now active allocation. This is preparation for eliminating all of the pointer receiver methods for RefPointer. --- .../pointerstore/pointer_reference.go | 31 +++++++++---------- .../pointerstore/pointer_reference_test.go | 4 +-- .../internal/pointerstore/pointer_store.go | 5 ++- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index b63e936..0b53b54 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -72,11 +72,11 @@ func (r *RefPointer) free() { meta.setGen(gen) } -// NB: In the future this method should return a new RefPointer with updated -// generation tag instead of modifying the method receiver. +// Sets the RefPointer's metadata to not-free and creates a new RefPointer with +// the correct generation tag. // //gcassert:noescape -func (r *RefPointer) allocFromFree() { +func (r *RefPointer) allocFromFree() RefPointer { meta := r.metadata() if !meta.isFree() { @@ -86,8 +86,8 @@ func (r *RefPointer) allocFromFree() { // This object is now allocated and is no long free meta.setNotFree() - // Update this reference so it's generatation tag matches the metadata - r.setGen(meta.gen()) + // Create new RefPointer with correct generation tag + return r.withGen(meta.gen()) } // This method re-allocates the memory location. When this method returns r @@ -99,8 +99,6 @@ func (r *RefPointer) Realloc() RefPointer { // Test that this reference is actually allowed to access the allocation r.accessibleActiveAddress() - newRef := *r - // Get metadata generation tag and increment it meta := r.metadata() gen := meta.gen() @@ -108,13 +106,7 @@ func (r *RefPointer) Realloc() RefPointer { // Set the new generation tag in both the metadata and reference meta.setGen(gen) - newRef.setGen(gen) - return newRef -} - -//gcassert:noescape -func (r *RefPointer) IsNil() bool { - return r.address.isNil() + return r.withGen(gen) } //gcassert:noescape @@ -129,6 +121,11 @@ func (r *RefPointer) Bytes(size int) []byte { return r.accessibleActiveAddress().bytes(size) } +//gcassert:noescape +func (r *RefPointer) IsNil() bool { + return r.address.isNil() +} + // Calling this method // // 1: looks up the metadata for this reference @@ -178,6 +175,8 @@ func (r *RefPointer) Gen() uint8 { } //gcassert:noescape -func (r *RefPointer) setGen(gen uint8) { - r.address = r.address.withGen(gen) +func (r *RefPointer) withGen(gen uint8) RefPointer { + return RefPointer{ + address: r.address.withGen(gen), + } } diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index b96eadc..b966abd 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -88,7 +88,7 @@ func TestGenerationDoesNotAppearInOtherFields(t *testing.T) { metadata := r.metadata() metadata.setGen(maxGen) - r.setGen(maxGen) + r = r.withGen(maxGen) assert.Equal(t, dataPtr, r.DataPtr()) assert.Equal(t, metaPtr, r.metaPtr()) @@ -141,7 +141,7 @@ func TestAllocFromFree(t *testing.T) { assert.Equal(t, uint8(0), r.Gen()) r.free() - r.allocFromFree() + r = r.allocFromFree() // The reference still points to the same metadata location assert.Equal(t, meta, r.metadata()) diff --git a/offheap/internal/pointerstore/pointer_store.go b/offheap/internal/pointerstore/pointer_store.go index 2e39793..7fbd2ca 100644 --- a/offheap/internal/pointerstore/pointer_store.go +++ b/offheap/internal/pointerstore/pointer_store.go @@ -129,9 +129,8 @@ func (s *Store) allocFromFree() (RefPointer, bool) { return RefPointer{}, false } - // set up the reference to be 'allocated' - r.allocFromFree() - return r, true + // Create new allocated reference + return r.allocFromFree(), true } func (s *Store) allocFromOffset() RefPointer { From e813ac51659a51d41092371800eb45db44bd0bc2 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 19:51:23 +1300 Subject: [PATCH 24/29] Remove pointer receivers from RefPointer methods Because the RefPointer is only one word wide, and no methods update the receiver, they return new modified RefPointers instead, we don't need pointer receiver semantics. This is a significant simplification of this part of the code-base. Previously all RefPointer, and all Ref-types in general, are used as values but have methods with pointer receivers. This caused some unexpected allocations which could be fixed, but with odd-looking contortions in the code and long comments to explain them. Now there are no pointer receivers, no subtle allocations. Happy day. --- .../pointerstore/pointer_reference.go | 64 ++++--------------- .../pointerstore/pointer_reference_test.go | 7 +- .../pointerstore/pointer_store_test.go | 5 +- 3 files changed, 19 insertions(+), 57 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 0b53b54..8cb626e 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -15,17 +15,6 @@ import ( // The generation must be masked out to get a usable pointer value. The object // pointed to must have the same generation value in order to access/free that // object. -// -// Because the Refpointer struct is two words (on 64 bit systems) in size, all -// method receivers are pointers to avoid copying two words in method calls. -// However, the RefPointer is _always_ used as a value. This means that having -// a pointer receiver could potentially cause the RefPointer to be allocated if -// its receiver escapes to the heap (according to escape analysis). We assert -// that all methods do not allow their receiver variable to escape to the heap. -// -// This is all very nice and good, but the idea that avoiding copying two words -// is a meaningful improvement are speculative and haven't been tested. This -// would be a good target for future performance testing. type RefPointer struct { address taggedAddress } @@ -55,8 +44,7 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { return r } -//gcassert:noescape -func (r *RefPointer) free() { +func (r RefPointer) free() { // Check that this reference can access the allocation to free it r.accessibleActiveAddress() @@ -74,13 +62,11 @@ func (r *RefPointer) free() { // Sets the RefPointer's metadata to not-free and creates a new RefPointer with // the correct generation tag. -// -//gcassert:noescape -func (r *RefPointer) allocFromFree() RefPointer { +func (r RefPointer) allocFromFree() RefPointer { meta := r.metadata() if !meta.isFree() { - panic(fmt.Errorf("attempt to alloc-from-free active allocation %v", *r)) + panic(fmt.Errorf("attempt to alloc-from-free active allocation %v", r)) } // This object is now allocated and is no long free @@ -93,9 +79,7 @@ func (r *RefPointer) allocFromFree() RefPointer { // This method re-allocates the memory location. When this method returns r // will no longer be a valid reference. The reference returned _will_ be a // valid reference to the same location. -// -//gcassert:noescape -func (r *RefPointer) Realloc() RefPointer { +func (r RefPointer) Realloc() RefPointer { // Test that this reference is actually allowed to access the allocation r.accessibleActiveAddress() @@ -109,20 +93,16 @@ func (r *RefPointer) Realloc() RefPointer { return r.withGen(gen) } -//gcassert:noescape -func (r *RefPointer) DataPtr() uintptr { +func (r RefPointer) DataPtr() uintptr { return r.accessibleActiveAddress().address() } // Convenient method to retrieve raw data of an allocation -// -//gcassert:noescape -func (r *RefPointer) Bytes(size int) []byte { +func (r RefPointer) Bytes(size int) []byte { return r.accessibleActiveAddress().bytes(size) } -//gcassert:noescape -func (r *RefPointer) IsNil() bool { +func (r RefPointer) IsNil() bool { return r.address.isNil() } @@ -131,20 +111,11 @@ func (r *RefPointer) IsNil() bool { // 1: looks up the metadata for this reference // 2: Verifies that the reference is _allowed_ to access this data // 3: Returns the taggedAddress pointing to the actual data -// -//gcassert:noescape -func (r *RefPointer) accessibleActiveAddress() taggedAddress { +func (r RefPointer) accessibleActiveAddress() taggedAddress { meta := r.metadata() if meta.isFree() { - // NB: We make a copy of r here - otherwise the compiler - // believes that r itself escapes to the heap (not strictly - // wrong) and will allocate it to the heap, even if this path - // is not taken. This panic path _does_ allocate due to the fmt - // call, but if we don't take a copy of r in the fmt call, then - // every call will allocate regardless of whether the method - // panics or not - panic(fmt.Errorf("attempt to access freed allocation %v", *r)) + panic(fmt.Errorf("attempt to access freed allocation %v", r)) } if meta.gen() != r.Gen() { @@ -154,28 +125,19 @@ func (r *RefPointer) accessibleActiveAddress() taggedAddress { return meta.dataAddressAndGen } -//gcassert:noescape -func (r *RefPointer) metaPtr() uintptr { - return r.metaAddress().address() -} - -//gcassert:noescape -func (r *RefPointer) metadata() *metadata { +func (r RefPointer) metadata() *metadata { return (*metadata)(unsafe.Pointer(r.metaAddress().address())) } -//gcassert:noescape -func (r *RefPointer) metaAddress() taggedAddress { +func (r RefPointer) metaAddress() taggedAddress { return r.address } -//gcassert:noescape -func (r *RefPointer) Gen() uint8 { +func (r RefPointer) Gen() uint8 { return r.address.gen() } -//gcassert:noescape -func (r *RefPointer) withGen(gen uint8) RefPointer { +func (r RefPointer) withGen(gen uint8) RefPointer { return RefPointer{ address: r.address.withGen(gen), } diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index b966abd..2a0d51a 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -84,14 +84,13 @@ func TestGenerationDoesNotAppearInOtherFields(t *testing.T) { r := NewReference(objects[0], metadatas[0]) dataPtr := r.DataPtr() - metaPtr := r.metaPtr() metadata := r.metadata() metadata.setGen(maxGen) r = r.withGen(maxGen) assert.Equal(t, dataPtr, r.DataPtr()) - assert.Equal(t, metaPtr, r.metaPtr()) + assert.Equal(t, metadata, r.metadata()) assert.Equal(t, uint8(maxGen), r.Gen()) } @@ -177,7 +176,7 @@ func TestRealloc(t *testing.T) { meta1 := r1.metadata() dataPtr := r1.DataPtr() - metaPtr := r1.metaPtr() + metadata := r1.metadata() gen := r1.Gen() r2 := r1.Realloc() @@ -192,7 +191,7 @@ func TestRealloc(t *testing.T) { // Assert that the data/metadata pointed to by r1 and r2 is the same assert.Equal(t, dataPtr, r2.DataPtr()) - assert.Equal(t, metaPtr, r2.metaPtr()) + assert.Equal(t, metadata, r2.metadata()) // Assert that r2 has a different generation than r1 assert.NotEqual(t, gen, r2.Gen()) diff --git a/offheap/internal/pointerstore/pointer_store_test.go b/offheap/internal/pointerstore/pointer_store_test.go index 26b051c..ede351e 100644 --- a/offheap/internal/pointerstore/pointer_store_test.go +++ b/offheap/internal/pointerstore/pointer_store_test.go @@ -7,6 +7,7 @@ package pointerstore import ( "fmt" "testing" + "unsafe" "github.com/stretchr/testify/assert" ) @@ -73,7 +74,7 @@ func TestSlabIntegrity(t *testing.T) { } baseSlabData := refs[0].DataPtr() - baseSlabMetadata := refs[0].metaPtr() + baseSlabMetadata := uintptr(unsafe.Pointer(refs[0].metadata())) // Check that the metadata is allocated immediately _after_ the data assert.Equal(t, baseSlabMetadata, baseSlabData+uintptr(conf.TotalObjectSize)) @@ -86,7 +87,7 @@ func TestSlabIntegrity(t *testing.T) { assert.Equal(t, baseSlabData+expectedDataOffset, dataPtr) // Check that the metadata allocations are spaced out appropriately - metaPtr := ref.metaPtr() + metaPtr := uintptr(unsafe.Pointer(ref.metadata())) expectedMetaOffset := uintptr(conf.MetadataSize) * uintptr(i) assert.Equal(t, baseSlabMetadata+expectedMetaOffset, metaPtr) } From cd137ce1c953519dcbb7873aab2dd931354b4ca9 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Tue, 4 Feb 2025 20:03:11 +1300 Subject: [PATCH 25/29] Add dirty benchmark --- offheap/object_bench_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 offheap/object_bench_test.go diff --git a/offheap/object_bench_test.go b/offheap/object_bench_test.go new file mode 100644 index 0000000..256d687 --- /dev/null +++ b/offheap/object_bench_test.go @@ -0,0 +1,33 @@ +package offheap + +import "testing" + +type benchStruct struct { + field int +} + +func BenchmarkAllocWriteRead(b *testing.B) { + os := New() + + refs := make([]RefObject[benchStruct], 0, b.N) + + for range b.N { + ref := AllocObject[benchStruct](os) + refs = append(refs, ref) + } + + b.ResetTimer() + b.ReportAllocs() + + for i := range b.N { + refs[i].Value().field = i + } + + sum := 0 + + for i := range b.N { + sum += refs[i].Value().field + } + + println(sum) +} From c128b4f1a20120f6e55725ccb4710781da4a5bf6 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Thu, 6 Feb 2025 20:11:16 +1300 Subject: [PATCH 26/29] Update metadata comments Clarify that metadata contains both the generation _and_ the is-free tag, which are used to check the integrity of RefPointer data accesses. Writing these comments it strikes me that metadata is responsible for the integrity checks when accessing data through Refpointers, but the check themselves are done inside RefPointer methods. It seems like moving this code into metadata will improve code clarity. --- offheap/internal/pointerstore/metadata.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go index 9372938..4947f62 100644 --- a/offheap/internal/pointerstore/metadata.go +++ b/offheap/internal/pointerstore/metadata.go @@ -4,13 +4,14 @@ package pointerstore -// If the object's metadata has a non-nil nextFree pointer then the object is -// currently free. Object's which have never been allocated are implicitly -// free, but have a nil nextFree. -// // A RefPointer smuggles a generation tag. Only references with the same gen // value can access/free objects they point to. This is a best-effort safety // check to try to catch use-after-free type errors. +// +// Metadata smuggles a generation tag and an is-free tag into its +// taggedAddress. When accessing the data from a RefPointer we check that the +// metadata indicates the object is not free and that the RefPointer's +// generation matches that of the metadat. type metadata struct { dataAddressAndGen taggedAddress } From d9e16a732ca64497a43447009babe11a9b0328fb Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Fri, 7 Feb 2025 19:00:58 +1300 Subject: [PATCH 27/29] Remove RefPointer.metaAddresss() This is just a getter for the address field, and can be replaced by a simple field access. It serves no clear purpose. --- offheap/internal/pointerstore/pointer_reference.go | 6 +----- offheap/internal/pointerstore/pointer_reference_test.go | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 8cb626e..5edec90 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -126,11 +126,7 @@ func (r RefPointer) accessibleActiveAddress() taggedAddress { } func (r RefPointer) metadata() *metadata { - return (*metadata)(unsafe.Pointer(r.metaAddress().address())) -} - -func (r RefPointer) metaAddress() taggedAddress { - return r.address + return (*metadata)(unsafe.Pointer(r.address.address())) } func (r RefPointer) Gen() uint8 { diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index 2a0d51a..09f70c1 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -55,7 +55,7 @@ func TestNewReference(t *testing.T) { // Bytes points to data at the correct location assert.Equal(t, objects[i], uintptr(unsafe.Pointer(&r.Bytes(8)[0]))) // Metadata pointer points to the correct location - assert.Equal(t, metadata[i], r.metaAddress().address()) + assert.Equal(t, metadata[i], r.address.address()) // Generation of a new Reference is always 0 assert.Equal(t, uint8(0), r.Gen()) // The data should be accessible through this reference From 3ea7e5fedafa5911d5127ba207fc43076d5ee38a Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Fri, 7 Feb 2025 19:01:33 +1300 Subject: [PATCH 28/29] taggedAddress.address() -> pointer() This method name more clearly reflects that a uintptr is being returned. It also more clearly indicates that we are switching from a taggedAddress, which is almost a pointer, but with a different name to avoid confusing it with a pointer, to an actual pointer type uintptr. --- .../internal/pointerstore/pointer_reference.go | 4 ++-- .../pointerstore/pointer_reference_test.go | 2 +- offheap/internal/pointerstore/tagged_address.go | 6 +++--- .../internal/pointerstore/tagged_address_test.go | 16 ++++++++-------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index 5edec90..c36e393 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -94,7 +94,7 @@ func (r RefPointer) Realloc() RefPointer { } func (r RefPointer) DataPtr() uintptr { - return r.accessibleActiveAddress().address() + return r.accessibleActiveAddress().pointer() } // Convenient method to retrieve raw data of an allocation @@ -126,7 +126,7 @@ func (r RefPointer) accessibleActiveAddress() taggedAddress { } func (r RefPointer) metadata() *metadata { - return (*metadata)(unsafe.Pointer(r.address.address())) + return (*metadata)(unsafe.Pointer(r.address.pointer())) } func (r RefPointer) Gen() uint8 { diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index 09f70c1..e7048cc 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -55,7 +55,7 @@ func TestNewReference(t *testing.T) { // Bytes points to data at the correct location assert.Equal(t, objects[i], uintptr(unsafe.Pointer(&r.Bytes(8)[0]))) // Metadata pointer points to the correct location - assert.Equal(t, metadata[i], r.address.address()) + assert.Equal(t, metadata[i], r.address.pointer()) // Generation of a new Reference is always 0 assert.Equal(t, uint8(0), r.Gen()) // The data should be accessible through this reference diff --git a/offheap/internal/pointerstore/tagged_address.go b/offheap/internal/pointerstore/tagged_address.go index 699ceca..8cbbe46 100644 --- a/offheap/internal/pointerstore/tagged_address.go +++ b/offheap/internal/pointerstore/tagged_address.go @@ -46,12 +46,12 @@ func (a taggedAddress) gen() uint8 { return (uint8)((a & genMask) >> maskShift) } -func (a taggedAddress) address() uintptr { +func (a taggedAddress) pointer() uintptr { return uintptr(a & addressMask) } func (a taggedAddress) bytes(size int) []byte { - return ([]byte)(unsafe.Slice((*byte)((unsafe.Pointer)(a.address())), size)) + return ([]byte)(unsafe.Slice((*byte)((unsafe.Pointer)(a.pointer())), size)) } func (a taggedAddress) isFree() bool { @@ -59,7 +59,7 @@ func (a taggedAddress) isFree() bool { } func (a taggedAddress) isNil() bool { - return a.address() == nilPtr + return a.pointer() == nilPtr } func (a taggedAddress) withGen(gen uint8) taggedAddress { diff --git a/offheap/internal/pointerstore/tagged_address_test.go b/offheap/internal/pointerstore/tagged_address_test.go index 546827f..d3da38c 100644 --- a/offheap/internal/pointerstore/tagged_address_test.go +++ b/offheap/internal/pointerstore/tagged_address_test.go @@ -20,7 +20,7 @@ func TestTaggedAddress_ZeroValue(t *testing.T) { // address and gen are 0 assert.True(t, ta.isNil()) - assert.Equal(t, uintptr(unsafe.Pointer(nil)), ta.address()) + assert.Equal(t, uintptr(unsafe.Pointer(nil)), ta.pointer()) assert.Equal(t, uint8(0), ta.gen()) } @@ -31,7 +31,7 @@ func TestTaggedAddress_NewTaggedAddress_Zero(t *testing.T) { // address and gen are 0 assert.True(t, ta.isNil()) - assert.Equal(t, nilPtr, ta.address()) + assert.Equal(t, nilPtr, ta.pointer()) assert.Equal(t, uint8(0), ta.gen()) } @@ -50,7 +50,7 @@ func TestTaggedAddress_AddressAndGenAreSeparate(t *testing.T) { // If the address is not 0 then isNil() must be false assert.Equal(t, newPtr == nilPtr, ta.isNil()) // Assert that the original address is preserved - assert.Equal(t, newPtr, ta.address()) + assert.Equal(t, newPtr, ta.pointer()) // Assert that the generation is preserved assert.Equal(t, gen, ta.gen()) } @@ -77,19 +77,19 @@ func TestTaggedAddress_IsFree(t *testing.T) { newPtr := uintptr(rand.Int63n(maxAddress + 1)) ta := newTaggedAddress(newPtr) - address := ta.address() + address := ta.pointer() gen := ta.gen() // A tagged address is created not-free. It is assumed that a // tagged address is only created for an allocation assert.False(t, ta.isFree()) // Neither the address nor the generation tag are changed by the is-free bit - assert.Equal(t, address, ta.address()) + assert.Equal(t, address, ta.pointer()) assert.Equal(t, gen, ta.gen()) ta = ta.withFree() assert.True(t, ta.isFree()) // Neither the address nor the generation tag are changed by the is-free bit - assert.Equal(t, address, ta.address()) + assert.Equal(t, address, ta.pointer()) assert.Equal(t, gen, ta.gen()) // Side-quest, set the generation tag and ensure this doesn't @@ -97,7 +97,7 @@ func TestTaggedAddress_IsFree(t *testing.T) { ta = ta.withGen(maxGen) assert.True(t, ta.isFree()) // Neither the address nor the generation tag are changed by the is-free bit - assert.Equal(t, address, ta.address()) + assert.Equal(t, address, ta.pointer()) assert.Equal(t, uint8(maxGen), ta.gen()) // Set the generation back @@ -106,7 +106,7 @@ func TestTaggedAddress_IsFree(t *testing.T) { ta = ta.withNotFree() assert.False(t, ta.isFree()) // Neither the address nor the generation tag are changed by the is-free bit - assert.Equal(t, address, ta.address()) + assert.Equal(t, address, ta.pointer()) assert.Equal(t, gen, ta.gen()) } } From 33a2f22812d7f35693d2c8b4565a0a22147715c9 Mon Sep 17 00:00:00 2001 From: Francis Stephens Date: Fri, 7 Feb 2025 23:49:41 +1300 Subject: [PATCH 29/29] Adding taggedAddress.withIncGen() method This method simply creates a new taggedAddress with an incremented generation tag. Only metadata increments the generation tag, while on RefPointer the generation tag is set directly. This is because when we allocFromFree a RefPointer the generation tag could be any legal value. This is not a critical change, but tidies up a little bit of code. Hopefully it is easier to read now. --- offheap/internal/pointerstore/metadata.go | 5 +- .../pointerstore/pointer_reference.go | 24 ++++---- .../pointerstore/pointer_reference_test.go | 12 ++-- .../internal/pointerstore/tagged_address.go | 15 ++++- .../pointerstore/tagged_address_test.go | 58 +++++++++++++++++++ 5 files changed, 92 insertions(+), 22 deletions(-) diff --git a/offheap/internal/pointerstore/metadata.go b/offheap/internal/pointerstore/metadata.go index 4947f62..ba88e27 100644 --- a/offheap/internal/pointerstore/metadata.go +++ b/offheap/internal/pointerstore/metadata.go @@ -22,8 +22,9 @@ func (m *metadata) gen() uint8 { } //gcassert:noescape -func (m *metadata) setGen(gen uint8) { - m.dataAddressAndGen = m.dataAddressAndGen.withGen(gen) +func (m *metadata) incGen() uint8 { + m.dataAddressAndGen = m.dataAddressAndGen.withIncGen() + return m.dataAddressAndGen.gen() } //gcassert:noescape diff --git a/offheap/internal/pointerstore/pointer_reference.go b/offheap/internal/pointerstore/pointer_reference.go index c36e393..5e2003c 100644 --- a/offheap/internal/pointerstore/pointer_reference.go +++ b/offheap/internal/pointerstore/pointer_reference.go @@ -33,9 +33,6 @@ func NewReference(dataAddress, metaAddress uintptr) RefPointer { } // Set the dataAddressAndGen in this reference's metadata. - // - // In one of the next steps we will use this to look up the actual data - // _and_ verify the generation of the reference. meta := r.metadata() meta.dataAddressAndGen = newTaggedAddress(dataAddress) // The value defaults to false, but we write it here for readability @@ -53,11 +50,8 @@ func (r RefPointer) free() { // Mark the object as free meta.setFree() - // Increment the generation for the allocation and set that generation in - // the Metadata and Reference - gen := meta.gen() - gen++ - meta.setGen(gen) + // Increment the generation for the allocation's metadata + meta.incGen() } // Sets the RefPointer's metadata to not-free and creates a new RefPointer with @@ -72,8 +66,11 @@ func (r RefPointer) allocFromFree() RefPointer { // This object is now allocated and is no long free meta.setNotFree() + // Get the metadata generation tag + gen := meta.gen() + // Create new RefPointer with correct generation tag - return r.withGen(meta.gen()) + return r.withGen(gen) } // This method re-allocates the memory location. When this method returns r @@ -83,13 +80,12 @@ func (r RefPointer) Realloc() RefPointer { // Test that this reference is actually allowed to access the allocation r.accessibleActiveAddress() - // Get metadata generation tag and increment it meta := r.metadata() - gen := meta.gen() - gen++ - // Set the new generation tag in both the metadata and reference - meta.setGen(gen) + // Get and increment the metadata generation tag + gen := meta.incGen() + + // Set the new generation tag in the reference return r.withGen(gen) } diff --git a/offheap/internal/pointerstore/pointer_reference_test.go b/offheap/internal/pointerstore/pointer_reference_test.go index e7048cc..6401c0a 100644 --- a/offheap/internal/pointerstore/pointer_reference_test.go +++ b/offheap/internal/pointerstore/pointer_reference_test.go @@ -86,12 +86,14 @@ func TestGenerationDoesNotAppearInOtherFields(t *testing.T) { dataPtr := r.DataPtr() metadata := r.metadata() - metadata.setGen(maxGen) - r = r.withGen(maxGen) + for i := 1; i <= maxGen; i++ { + gen := metadata.incGen() + r = r.withGen(gen) - assert.Equal(t, dataPtr, r.DataPtr()) - assert.Equal(t, metadata, r.metadata()) - assert.Equal(t, uint8(maxGen), r.Gen()) + assert.Equal(t, dataPtr, r.DataPtr()) + assert.Equal(t, metadata, r.metadata()) + assert.Equal(t, uint8(i%128), r.Gen()) + } } func TestFree(t *testing.T) { diff --git a/offheap/internal/pointerstore/tagged_address.go b/offheap/internal/pointerstore/tagged_address.go index 8cbbe46..6998882 100644 --- a/offheap/internal/pointerstore/tagged_address.go +++ b/offheap/internal/pointerstore/tagged_address.go @@ -20,6 +20,9 @@ const ( isFreeMask = taggedAddress(0x80 << maskShift) // Next 7 bits indicate generation tag genMask = taggedAddress(maxGen << maskShift) + // A generation value of one. This is used to increment generation + // values + genOne = taggedAddress(1 << maskShift) // Mask revealing all 8 tag bits tagMask = isFreeMask | genMask // Mask revealing 56 address bits @@ -63,7 +66,17 @@ func (a taggedAddress) isNil() bool { } func (a taggedAddress) withGen(gen uint8) taggedAddress { - return (a & (addressAndIsFreeMask)) | (taggedAddress(gen) << maskShift) + // move the new gen into postion + newGen := (taggedAddress(gen) << maskShift) & genMask + // Set the new generation value back into the address + return (a & addressAndIsFreeMask) | newGen +} + +func (a taggedAddress) withIncGen() taggedAddress { + // increment the generation value alone + newGen := (a + genOne) & genMask + // Set the new generation value back into the address + return (a & addressAndIsFreeMask) | newGen } func (a taggedAddress) withFree() taggedAddress { diff --git a/offheap/internal/pointerstore/tagged_address_test.go b/offheap/internal/pointerstore/tagged_address_test.go index d3da38c..0a44a55 100644 --- a/offheap/internal/pointerstore/tagged_address_test.go +++ b/offheap/internal/pointerstore/tagged_address_test.go @@ -57,6 +57,64 @@ func TestTaggedAddress_AddressAndGenAreSeparate(t *testing.T) { } } +// For any legal address and generation tag combination the address and +// generation tag are available unaltered +func TestTaggedAddress_WithGen(t *testing.T) { + for range 10 { + newPtr := uintptr(rand.Int63n(maxAddress + 1)) + ta := newTaggedAddress(newPtr) + testWithGen(t, ta.withNotFree()) + testWithGen(t, ta.withFree()) + } +} + +func testWithGen(t *testing.T, ta taggedAddress) { + ptr := ta.pointer() + isFree := ta.isFree() + for i := 0; i <= maxGen*4; i++ { + // increment tagged address generation + ta = ta.withGen(uint8(i)) + + // If the address is not 0 then isNil() must be false + assert.Equal(t, ptr == nilPtr, ta.isNil()) + // Assert that the original address is preserved + assert.Equal(t, ptr, ta.pointer()) + // Assert if the address remains free or not free + assert.Equal(t, isFree, ta.isFree()) + // Assert that the generation is preserved + assert.Equal(t, uint8(i%128), ta.gen()) + } +} + +// For any legal address and generation tag combination the address and +// generation tag are available unaltered +func TestTaggedAddress_IncGen(t *testing.T) { + for range 10 { + newPtr := uintptr(rand.Int63n(maxAddress + 1)) + ta := newTaggedAddress(newPtr) + testIncGen(t, ta.withNotFree()) + testIncGen(t, ta.withFree()) + } +} + +func testIncGen(t *testing.T, ta taggedAddress) { + ptr := ta.pointer() + isFree := ta.isFree() + for i := 0; i <= maxGen*4; i++ { + // If the address is not 0 then isNil() must be false + assert.Equal(t, ptr == nilPtr, ta.isNil()) + // Assert that the original address is preserved + assert.Equal(t, ptr, ta.pointer()) + // Assert if the address remains free or not free + assert.Equal(t, isFree, ta.isFree()) + // Assert that the generation is preserved + assert.Equal(t, uint8(i%128), ta.gen()) + + // increment tagged address generation + ta = ta.withIncGen() + } +} + // For any address with bits set in the generation tag region (i.e. upper 8 // bits) newTaggedAddress() panics func TestTaggedAddress_PointerWithGenBitsPanics(t *testing.T) {