From 22fed7834f82654873960c40ce3f64d6a18e3d46 Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Sun, 28 Dec 2025 20:43:20 +1100 Subject: [PATCH 1/9] support_uninitialised_field_for_skipping_pre_zeroing --- c++/src/capnp/arena.c++ | 16 ++- c++/src/capnp/arena.h | 20 ++- c++/src/capnp/compiler/capnpc-c++.c++ | 9 ++ c++/src/capnp/layout.c++ | 48 ++++++- c++/src/capnp/layout.h | 2 + c++/src/capnp/message-test.c++ | 177 ++++++++++++++++++++++++++ c++/src/capnp/message.c++ | 50 +++++++- c++/src/capnp/message.h | 20 ++- 8 files changed, 319 insertions(+), 23 deletions(-) diff --git a/c++/src/capnp/arena.c++ b/c++/src/capnp/arena.c++ index 7e6c4ba7ad..400ffe1577 100644 --- a/c++/src/capnp/arena.c++ +++ b/c++/src/capnp/arena.c++ @@ -165,7 +165,7 @@ BuilderArena::BuilderArena(MessageBuilder* message, : message(message), segment0(this, SegmentId(0), segments[0].space.begin(), verifySegment(segments[0].space), - &this->dummyLimiter, verifySegmentSize(segments[0].wordsUsed)) { + &this->dummyLimiter, verifySegmentSize(segments[0].wordsUsed), !segments[0].isZeroed) { if (segments.size() > 1) { kj::Vector> builders(segments.size() - 1); @@ -173,7 +173,7 @@ BuilderArena::BuilderArena(MessageBuilder* message, for (auto& segment: segments.slice(1, segments.size())) { builders.add(kj::heap( this, SegmentId(i++), segment.space.begin(), verifySegment(segment.space), - &this->dummyLimiter, verifySegmentSize(segment.wordsUsed))); + &this->dummyLimiter, verifySegmentSize(segment.wordsUsed), !segment.isZeroed)); } kj::Vector> forOutput; @@ -229,10 +229,13 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { kj::ArrayPtr ptr = message->allocateSegment(unbound(amount / WORDS)); auto actualSize = verifySegment(ptr); + // Check dirtiness + bool dirty = !message->isAllocationZeroed(); + // Re-allocate segment0 in-place. This is a bit of a hack, but we have not returned any // pointers to this segment yet, so it should be fine. kj::dtor(segment0); - kj::ctor(segment0, this, SegmentId(0), ptr.begin(), actualSize, &this->dummyLimiter); + kj::ctor(segment0, this, SegmentId(0), ptr.begin(), actualSize, &this->dummyLimiter, ZERO * WORDS, dirty); segmentWithSpace = &segment0; return AllocateResult { &segment0, segment0.allocate(amount) }; @@ -252,7 +255,8 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { } // Need to allocate a new segment. - SegmentBuilder* result = addSegmentInternal(message->allocateSegment(unbound(amount / WORDS))); + bool dirty = !message->isAllocationZeroed(); + SegmentBuilder* result = addSegmentInternal(message->allocateSegment(unbound(amount / WORDS)), dirty); // Check this new segment first the next time we need to allocate. segmentWithSpace = result; @@ -267,7 +271,7 @@ SegmentBuilder* BuilderArena::addExternalSegment(kj::ArrayPtr conten } template -SegmentBuilder* BuilderArena::addSegmentInternal(kj::ArrayPtr content) { +SegmentBuilder* BuilderArena::addSegmentInternal(kj::ArrayPtr content, bool possiblyDirty) { // This check should never fail in practice, since you can't get an Orphanage without allocating // the root segment. KJ_REQUIRE(segment0.getArena() != nullptr, @@ -286,7 +290,7 @@ SegmentBuilder* BuilderArena::addSegmentInternal(kj::ArrayPtr content) { kj::Own newBuilder = kj::heap( this, SegmentId(segmentState->builders.size() + 1), - content.begin(), contentSize, &this->dummyLimiter); + content.begin(), contentSize, &this->dummyLimiter, ZERO * WORDS, possiblyDirty); SegmentBuilder* result = newBuilder.get(); segmentState->builders.add(kj::mv(newBuilder)); diff --git a/c++/src/capnp/arena.h b/c++/src/capnp/arena.h index 7308912b80..63f5d1a201 100644 --- a/c++/src/capnp/arena.h +++ b/c++/src/capnp/arena.h @@ -185,9 +185,11 @@ class SegmentReader { class SegmentBuilder: public SegmentReader { public: inline SegmentBuilder(BuilderArena* arena, SegmentId id, word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter, SegmentWordCount wordsUsed = ZERO * WORDS); + ReadLimiter* readLimiter, SegmentWordCount wordsUsed = ZERO * WORDS, + bool possiblyDirty = false); inline SegmentBuilder(BuilderArena* arena, SegmentId id, const word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter); + ReadLimiter* readLimiter, SegmentWordCount wordsUsed = ZERO * WORDS, + bool possiblyDirty = false); inline SegmentBuilder(BuilderArena* arena, SegmentId id, decltype(nullptr), ReadLimiter* readLimiter); @@ -208,6 +210,8 @@ class SegmentBuilder: public SegmentReader { inline bool isWritable() { return !readOnly; } + inline bool isPossiblyDirty() { return possiblyDirty; } + inline void tryTruncate(word* from, word* to); // If `from` points just past the current end of the segment, then move the end back to `to`. // Otherwise, do nothing. @@ -224,6 +228,8 @@ class SegmentBuilder: public SegmentReader { bool readOnly; + bool possiblyDirty; + [[noreturn]] void throwNotWritable(); KJ_DISALLOW_COPY_AND_MOVE(SegmentBuilder); @@ -376,7 +382,7 @@ class BuilderArena final: public Arena { // segment that is already-full, in which case we don't update this pointer. template // Can be `word` or `const word`. - SegmentBuilder* addSegmentInternal(kj::ArrayPtr content); + SegmentBuilder* addSegmentInternal(kj::ArrayPtr content, bool possiblyDirty = false); }; // ======================================================================================= @@ -452,16 +458,16 @@ inline void SegmentReader::unread(WordCount64 amount) { readLimiter->unread(amou inline SegmentBuilder::SegmentBuilder( BuilderArena* arena, SegmentId id, word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter, SegmentWordCount wordsUsed) + ReadLimiter* readLimiter, SegmentWordCount wordsUsed, bool possiblyDirty) : SegmentReader(arena, id, ptr, size, readLimiter), - pos(ptr + wordsUsed), readOnly(false) {} + pos(ptr + wordsUsed), readOnly(false), possiblyDirty(possiblyDirty) {} inline SegmentBuilder::SegmentBuilder( BuilderArena* arena, SegmentId id, const word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter) + ReadLimiter* readLimiter, SegmentWordCount /*wordsUsed*/, bool possiblyDirty) : SegmentReader(arena, id, ptr, size, readLimiter), // const_cast is safe here because the member won't ever be dereferenced because it appears // to point to the end of the segment anyway. - pos(const_cast(ptr + size)), readOnly(true) {} + pos(const_cast(ptr + size)), readOnly(true), possiblyDirty(possiblyDirty) {} inline SegmentBuilder::SegmentBuilder(BuilderArena* arena, SegmentId id, decltype(nullptr), ReadLimiter* readLimiter) : SegmentReader(arena, id, nullptr, ZERO * WORDS, readLimiter), diff --git a/c++/src/capnp/compiler/capnpc-c++.c++ b/c++/src/capnp/compiler/capnpc-c++.c++ index 262ccb726b..3661acb4b9 100644 --- a/c++/src/capnp/compiler/capnpc-c++.c++ +++ b/c++/src/capnp/compiler/capnpc-c++.c++ @@ -1756,6 +1756,8 @@ private: " inline ::capnp::BuilderFor init", titleCase, "As(unsigned int size);\n"), COND(!shouldTemplatizeInit, " ", maybeInline, builderType, " init", titleCase, "(unsigned int size);\n")), + COND(shouldIncludeSizedInit && !shouldTemplatizeInit && typeSchema.which() == schema::Type::DATA, + " ", maybeInline, builderType, " init", titleCase, "Uninitialized(unsigned int size);\n"), " ", maybeInline, "void adopt", titleCase, "(::capnp::Orphan<", type, ">&& value);\n" " ", maybeInline, "::capnp::Orphan<", type, "> disown", titleCase, "();\n", COND(shouldExcludeInLiteMode, "#endif // !CAPNP_LITE\n"), @@ -1826,6 +1828,13 @@ private: " return ::capnp::_::PointerHelpers<", type, ">::init(_builder.getPointerField(\n" " ::capnp::bounded<", offset, ">() * ::capnp::POINTERS), size);\n" "}\n"), + COND(shouldIncludeSizedInit && !shouldTemplatizeInit && typeSchema.which() == schema::Type::DATA, + templateContext.allDecls(), + maybeInline, builderType, " ", scope, "Builder::init", titleCase, "Uninitialized(unsigned int size) {\n", + unionDiscrim.set, + " return _builder.getPointerField(\n" + " ::capnp::bounded<", offset, ">() * ::capnp::POINTERS).initBlobUninitialized<", type, ">(size);\n" + "}\n"), templateContext.allDecls(), maybeInline, "void ", scope, "Builder::adopt", titleCase, "(\n" " ::capnp::Orphan<", type, ">&& value) {\n", diff --git a/c++/src/capnp/layout.c++ b/c++/src/capnp/layout.c++ index 372fddcff9..7d5f60a033 100644 --- a/c++/src/capnp/layout.c++ +++ b/c++/src/capnp/layout.c++ @@ -458,7 +458,7 @@ struct WireHelpers { static KJ_ALWAYS_INLINE(word* allocate( WirePointer*& ref, SegmentBuilder*& segment, CapTableBuilder* capTable, - SegmentWordCount amount, WirePointer::Kind kind, BuilderArena* orphanArena)) { + SegmentWordCount amount, WirePointer::Kind kind, BuilderArena* orphanArena, bool zeroMemory = true)) { // Allocate space in the message for a new object, creating far pointers if necessary. The // space is guaranteed to be zero'd (because MessageBuilder implementations are required to // return zero'd memory). @@ -515,10 +515,21 @@ struct WireHelpers { ref = reinterpret_cast(ptr); ref->setKindAndTarget(kind, ptr + POINTER_SIZE_IN_WORDS, segment); + // 如果需要清零,并且 segment 是 dirty 的,则清零 landing pad 之后的数据部分 + // Landing pad (ptr) 已经被 setKindAndTarget 写过了,不需要清零 + if (zeroMemory && segment->isPossiblyDirty()) { + WireHelpers::zeroMemory(ptr + POINTER_SIZE_IN_WORDS, amount); + } + // Allocated space follows new pointer. return ptr + POINTER_SIZE_IN_WORDS; } else { ref->setKindAndTarget(kind, ptr, segment); + + // 如果需要清零且 segment 可能是脏的 + if (zeroMemory && segment->isPossiblyDirty()) { + WireHelpers::zeroMemory(ptr, amount); + } return ptr; } } else { @@ -527,6 +538,10 @@ struct WireHelpers { auto allocation = orphanArena->allocate(amount); segment = allocation.segment; ref->setKindForOrphan(kind); + // OrphanArena 分配的也需要检查 dirty + if (zeroMemory && segment->isPossiblyDirty()) { + WireHelpers::zeroMemory(allocation.words, amount); + } return allocation.words; } } @@ -1717,6 +1732,32 @@ struct WireHelpers { return { segment, Data::Builder(reinterpret_cast(ptr), unbound(size / BYTES)) }; } + static KJ_ALWAYS_INLINE(SegmentAnd initDataPointerUninitialized( + WirePointer* ref, SegmentBuilder* segment, CapTableBuilder* capTable, BlobSize size, + BuilderArena* orphanArena = nullptr)) { + // Allocate with zeroMemory = false for performance. + word* ptr = allocate(ref, segment, capTable, roundBytesUpToWords(size), + WirePointer::LIST, orphanArena, false); + + // Initialize the pointer. + ref->listRef.set(ElementSize::BYTE, size * (ONE * ELEMENTS / BYTES)); + + // Security: Zero-out padding bytes if memory is dirty. + if (segment->isPossiblyDirty()) { + size_t byteSize = unbound(size / BYTES); + // Calculate the actual allocated size in bytes (aligned to Word boundary). + size_t allocatedSize = unbound(roundBytesUpToWords(size)) * sizeof(word); + + if (byteSize < allocatedSize) { + // Zero-out the trailing padding bytes to prevent information leakage. + memset(reinterpret_cast(ptr) + byteSize, 0, allocatedSize - byteSize); + } + } + + // Build the Data::Builder. + return { segment, Data::Builder(reinterpret_cast(ptr), unbound(size / BYTES)) }; + } + static KJ_ALWAYS_INLINE(SegmentAnd setDataPointer( WirePointer* ref, SegmentBuilder* segment, CapTableBuilder* capTable, Data::Reader value, BuilderArena* orphanArena = nullptr)) { @@ -2595,6 +2636,11 @@ Data::Builder PointerBuilder::initBlob(ByteCount size) { assertMaxBits(size, ThrowOverflow())).value; } template <> +Data::Builder PointerBuilder::initBlobUninitialized(ByteCount size) { + return WireHelpers::initDataPointerUninitialized(pointer, segment, capTable, + assertMaxBits(size, ThrowOverflow())).value; +} +template <> void PointerBuilder::setBlob(Data::Reader value) { WireHelpers::setDataPointer(pointer, segment, capTable, value); } diff --git a/c++/src/capnp/layout.h b/c++/src/capnp/layout.h index dcb0371111..1defc35722 100644 --- a/c++/src/capnp/layout.h +++ b/c++/src/capnp/layout.h @@ -358,6 +358,7 @@ class PointerBuilder: public kj::DisallowConstCopy { ListBuilder initList(ElementSize elementSize, ElementCount elementCount); ListBuilder initStructList(ElementCount elementCount, StructSize size); template typename T::Builder initBlob(ByteCount size); + template typename T::Builder initBlobUninitialized(ByteCount size); // Init methods: Initialize the pointer to a newly-allocated object, discarding the existing // object. @@ -967,6 +968,7 @@ template <> typename Text::Reader PointerReader::getBlob( const void* defaultValue, ByteCount defaultSize) const; template <> typename Data::Builder PointerBuilder::initBlob(ByteCount size); +template <> typename Data::Builder PointerBuilder::initBlobUninitialized(ByteCount size); template <> void PointerBuilder::setBlob(typename Data::Reader value); template <> typename Data::Builder PointerBuilder::getBlob( const void* defaultValue, ByteCount defaultSize); diff --git a/c++/src/capnp/message-test.c++ b/c++/src/capnp/message-test.c++ index bb5acca711..f47c6d8de1 100644 --- a/c++/src/capnp/message-test.c++ +++ b/c++/src/capnp/message-test.c++ @@ -25,6 +25,7 @@ #include #include #include +#include namespace capnp { namespace _ { // private @@ -213,6 +214,182 @@ KJ_TEST("MessageBuilder::sizeInWords()") { KJ_EXPECT(reader.sizeInWords() == expected); } +KJ_TEST("MallocMessageBuilder with NO_ZERO_MEMORY strategy") { + // Verify that the new InitializationStrategy API works for basic usage. + MallocMessageBuilder builder(1024, AllocationStrategy::FIXED_SIZE, + MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + auto root = builder.initRoot(); + root.setInt64Field(12345); + KJ_EXPECT(root.getInt64Field() == 12345); +} + +KJ_TEST("SECURITY: Uninitialized DATA zeroes padding bytes (Info Leak Prevention)") { + // Setup a "Dirty" Scratch Buffer filled with 0xAA. + // This simulates a scenario where malloc returns memory containing sensitive residual data. + byte dirtyBuffer[1024]; + memset(dirtyBuffer, 0xAA, sizeof(dirtyBuffer)); + kj::ArrayPtr scratch(reinterpret_cast(dirtyBuffer), sizeof(dirtyBuffer) / sizeof(word)); + + // Create a builder that opts out of zeroing. + MallocMessageBuilder builder(scratch, AllocationStrategy::FIXED_SIZE, + MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + + auto root = builder.initRoot(); + + // Allocate 3 bytes of DATA. + // Physical layout: 1 word (8 bytes) allocated. + // Bytes [0, 1, 2] are the data body. + // Bytes [3, 4, 5, 6, 7] are alignment padding. + auto data = root.initDataFieldUninitialized(3); + const byte* rawPtr = data.begin(); + + // CHECK 1: Performance verification. + // The body bytes [0..2] should remain 0xAA. + // This proves that we successfully skipped the memset for the data payload. + KJ_EXPECT(rawPtr[0] == 0xAA); + KJ_EXPECT(rawPtr[1] == 0xAA); + KJ_EXPECT(rawPtr[2] == 0xAA); + + // CHECK 2: Security verification. + // The padding bytes [3..7] MUST be zeroed. + // Leaving these as 0xAA would constitute an Information Leak vulnerability. + KJ_EXPECT(rawPtr[3] == 0x00); + KJ_EXPECT(rawPtr[4] == 0x00); + KJ_EXPECT(rawPtr[5] == 0x00); + KJ_EXPECT(rawPtr[6] == 0x00); + KJ_EXPECT(rawPtr[7] == 0x00); +} + +KJ_TEST("SAFETY: Structs are always zeroed even with NO_ZERO_MEMORY") { + // Setup a dirty buffer (filled with 0xBB). + byte dirtyBuffer[1024]; + memset(dirtyBuffer, 0xBB, sizeof(dirtyBuffer)); + kj::ArrayPtr scratch(reinterpret_cast(dirtyBuffer), sizeof(dirtyBuffer) / sizeof(word)); + + MallocMessageBuilder builder(scratch, AllocationStrategy::FIXED_SIZE, + MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + + // Initialize a Struct. + // Even though the builder is in NO_ZERO_MEMORY mode, Struct allocations must always + // enforce zero-initialization to ensure pointer validity and default values. + auto root = builder.initRoot(); + + // Verify fields are 0/false, not 0xBB. + KJ_EXPECT(root.getInt64Field() == 0); + KJ_EXPECT(root.getUInt32Field() == 0); + KJ_EXPECT(root.getBoolField() == false); +} + +KJ_TEST("Corner Case: Uninitialized DATA with exact word alignment") { + // Test boundary condition: Size is exactly 8 bytes (No padding). + byte dirtyBuffer[1024]; + memset(dirtyBuffer, 0xCC, sizeof(dirtyBuffer)); + kj::ArrayPtr scratch(reinterpret_cast(dirtyBuffer), sizeof(dirtyBuffer) / sizeof(word)); + + MallocMessageBuilder builder(scratch, AllocationStrategy::FIXED_SIZE, + MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + auto root = builder.initRoot(); + + // 8 bytes -> Exactly 1 word. Padding size is 0. + // The padding zeroing logic must not crash or overwrite the next word. + auto data = root.initDataFieldUninitialized(8); + const byte* rawPtr = data.begin(); + + // Body is dirty (0xCC). + for(int i = 0; i < 8; ++i) { + KJ_EXPECT(rawPtr[i] == 0xCC); + } + + // Verify we didn't touch the *next* word (which should still be 0xCC). + // Note: Accessing rawPtr[8] is technically out of bounds for the 'data' blob, + // but valid within our controlled scratch buffer context. + KJ_EXPECT(rawPtr[8] == 0xCC); +} + +KJ_TEST("Corner Case: Uninitialized DATA with 1 byte padding") { + // Test boundary condition: Size is 7 bytes (1 byte padding). + byte dirtyBuffer[1024]; + memset(dirtyBuffer, 0xDD, sizeof(dirtyBuffer)); + kj::ArrayPtr scratch(reinterpret_cast(dirtyBuffer), sizeof(dirtyBuffer) / sizeof(word)); + + MallocMessageBuilder builder(scratch, AllocationStrategy::FIXED_SIZE, + MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + auto root = builder.initRoot(); + + auto data = root.initDataFieldUninitialized(7); + const byte* rawPtr = data.begin(); + + // Body [0..6] is dirty. + for(int i = 0; i < 7; ++i) { + KJ_EXPECT(rawPtr[i] == 0xDD); + } + + // The single padding byte [7] must be zeroed. + KJ_EXPECT(rawPtr[7] == 0x00); +} + +// Helper class for the Custom Builder test. +// Simulates a user-defined MessageBuilder that uses a custom allocator (e.g., malloc) +// and does not zero memory, overriding isAllocationZeroed() to return false. +class DirtyMallocMessageBuilder : public MessageBuilder { +public: + DirtyMallocMessageBuilder() + : MessageBuilder(kj::heapArray({ + // Initialize with nullptr. The Arena will call allocateSegment() on first use. + SegmentInit { nullptr, 0, false } + })) {} + + ~DirtyMallocMessageBuilder() { + for (void* ptr : allocations) { + free(ptr); + } + } + + // CRITICAL: Explicitly declare that this allocator provides dirty memory. + bool isAllocationZeroed() const override { return false; } + + kj::ArrayPtr allocateSegment(uint minimumSize) override { + size_t sizeBytes = minimumSize * sizeof(word); + void* ptr = malloc(sizeBytes); + KJ_ASSERT(ptr != nullptr); + + // Deliberately fill with garbage (0xEE) to verify lazy zeroing logic. + memset(ptr, 0xEE, sizeBytes); + + allocations.add(ptr); + return kj::arrayPtr(reinterpret_cast(ptr), minimumSize); + } + +private: + kj::Vector allocations; +}; + +KJ_TEST("CustomMessageBuilder: Lazy zeroing works with custom dirty allocator") { + DirtyMallocMessageBuilder builder; + + // 1. Verify Safety: Structs must be zeroed. + auto root = builder.initRoot(); + KJ_EXPECT(root.getInt64Field() == 0); + KJ_EXPECT(root.getBoolField() == false); + + // 2. Verify Performance & Security for Data Blob. + // Allocate 3 bytes. Expect 0xEE in body, 0x00 in padding. + auto data = root.initDataFieldUninitialized(3); + const byte* ptr = data.begin(); + + // Check Body (Dirty / Performance) + KJ_EXPECT(ptr[0] == 0xEE); + KJ_EXPECT(ptr[1] == 0xEE); + KJ_EXPECT(ptr[2] == 0xEE); + + // Check Padding (Clean / Security) + KJ_EXPECT(ptr[3] == 0x00); + KJ_EXPECT(ptr[4] == 0x00); + KJ_EXPECT(ptr[5] == 0x00); + KJ_EXPECT(ptr[6] == 0x00); + KJ_EXPECT(ptr[7] == 0x00); +} + // TODO(test): More tests. } // namespace diff --git a/c++/src/capnp/message.c++ b/c++/src/capnp/message.c++ index d08caf9d62..077b62d6de 100644 --- a/c++/src/capnp/message.c++ +++ b/c++/src/capnp/message.c++ @@ -210,21 +210,47 @@ kj::ArrayPtr SegmentArrayMessageReader::getSegment(uint id) { // ------------------------------------------------------------------- MallocMessageBuilder::MallocMessageBuilder( - uint firstSegmentWords, AllocationStrategy allocationStrategy) - : nextSize(firstSegmentWords), allocationStrategy(allocationStrategy), + uint firstSegmentWords, AllocationStrategy allocationStrategy, InitializationStrategy initStrategy) + : nextSize(firstSegmentWords), allocationStrategy(allocationStrategy), initializationStrategy(initStrategy), ownFirstSegment(true), returnedFirstSegment(false), firstSegment(nullptr) {} MallocMessageBuilder::MallocMessageBuilder( - kj::ArrayPtr firstSegment, AllocationStrategy allocationStrategy) - : nextSize(firstSegment.size()), allocationStrategy(allocationStrategy), + kj::ArrayPtr firstSegment, AllocationStrategy allocationStrategy, InitializationStrategy initStrategy) + : nextSize(firstSegment.size()), allocationStrategy(allocationStrategy), initializationStrategy(initStrategy), ownFirstSegment(false), returnedFirstSegment(false), firstSegment(firstSegment.begin()) { KJ_REQUIRE(firstSegment.size() > 0, "First segment size must be non-zero."); + if (initStrategy == InitializationStrategy::NO_ZERO_MEMORY) { + memset(firstSegment.begin(), 0, sizeof(word)); + } + // Checking just the first word should catch most cases of failing to zero the segment. KJ_REQUIRE(*reinterpret_cast(firstSegment.begin()) == 0, "First segment must be zeroed."); } +// MallocMessageBuilder::MallocMessageBuilder( +// kj::ArrayPtr firstSegment, AllocationStrategy allocationStrategy, InitializationStrategy initStrategy) +// : MessageBuilder(kj::heapArray({ +// SegmentInit { +// firstSegment, +// 0, +// initStrategy == InitializationStrategy::ZERO_MEMORY +// } +// })), +// nextSize(firstSegment.size()), allocationStrategy(allocationStrategy), +// initializationStrategy(initStrategy), +// ownFirstSegment(false), returnedFirstSegment(false), firstSegment(firstSegment.begin()) { +// +// KJ_REQUIRE(firstSegment.size() > 0, "First segment size must be non-zero."); +// +// if (initStrategy == InitializationStrategy::ZERO_MEMORY) { +// // Checking just the first word should catch most cases of failing to zero the segment. +// KJ_REQUIRE(*reinterpret_cast(firstSegment.begin()) == 0, +// "First segment must be zeroed."); +// } +// } + MallocMessageBuilder::~MallocMessageBuilder() noexcept(false) { if (returnedFirstSegment) { if (ownFirstSegment) { @@ -265,9 +291,19 @@ kj::ArrayPtr MallocMessageBuilder::allocateSegment(uint minimumSize) { uint size = kj::max(minimumSize, nextSize); - void* result = calloc(size, sizeof(word)); - if (result == nullptr) { - KJ_FAIL_SYSCALL("calloc(size, sizeof(word))", ENOMEM, size); + void* result; + // 根据策略选择分配方式 + if (initializationStrategy == InitializationStrategy::ZERO_MEMORY) { + result = calloc(size, sizeof(word)); + if (result == nullptr) { + KJ_FAIL_SYSCALL("calloc(size, sizeof(word))", ENOMEM, size); + } + } else { + // NO_ZERO_MEMORY: 使用 malloc,不进行清零 + result = malloc(size * sizeof(word)); + if (result == nullptr) { + KJ_FAIL_SYSCALL("malloc(size * sizeof(word))", ENOMEM, size); + } } if (!returnedFirstSegment) { diff --git a/c++/src/capnp/message.h b/c++/src/capnp/message.h index 8ca385eadf..3f2535590f 100644 --- a/c++/src/capnp/message.h +++ b/c++/src/capnp/message.h @@ -168,6 +168,8 @@ class MessageBuilder { size_t wordsUsed; // Number of words in `space` which are used; the rest are free space in which additional // objects may be allocated. + + bool isZeroed = true; }; explicit MessageBuilder(kj::ArrayPtr segments); @@ -202,6 +204,8 @@ class MessageBuilder { // because otherwise the Cap'n Proto implementation would have to zero the memory anyway, and // many allocators are able to provide already-zero'd memory more efficiently. + virtual bool isAllocationZeroed() const { return true; } + template typename RootType::Builder initRoot(); // Initialize the root struct of the message as the given struct type. @@ -378,8 +382,14 @@ class MallocMessageBuilder: public MessageBuilder { // a specific location in memory. public: + enum class InitializationStrategy: uint8_t { + ZERO_MEMORY, + NO_ZERO_MEMORY + }; + explicit MallocMessageBuilder(uint firstSegmentWords = SUGGESTED_FIRST_SEGMENT_WORDS, - AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY); + AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY, + InitializationStrategy initStrategy = InitializationStrategy::ZERO_MEMORY); // Creates a BuilderContext which allocates at least the given number of words for the first // segment, and then uses the given strategy to decide how much to allocate for subsequent // segments. When choosing a value for firstSegmentWords, consider that: @@ -393,7 +403,8 @@ class MallocMessageBuilder: public MessageBuilder { // have reason to believe you need to. explicit MallocMessageBuilder(kj::ArrayPtr firstSegment, - AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY); + AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY, + InitializationStrategy initStrategy = InitializationStrategy::ZERO_MEMORY); // This version always returns the given array for the first segment, and then proceeds with the // allocation strategy. This is useful for optimization when building lots of small messages in // a tight loop: you can reuse the space for the first segment. @@ -401,6 +412,10 @@ class MallocMessageBuilder: public MessageBuilder { // firstSegment MUST be zero-initialized. MallocMessageBuilder's destructor will write new zeros // over any space that was used so that it can be reused. + virtual bool isAllocationZeroed() const override { + return initializationStrategy == InitializationStrategy::ZERO_MEMORY; + } + KJ_DISALLOW_COPY_AND_MOVE(MallocMessageBuilder); virtual ~MallocMessageBuilder() noexcept(false); @@ -409,6 +424,7 @@ class MallocMessageBuilder: public MessageBuilder { private: uint nextSize; AllocationStrategy allocationStrategy; + InitializationStrategy initializationStrategy; bool ownFirstSegment; bool returnedFirstSegment; From 367f9109b90fc0628357eb8edaaa16ee293541e9 Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Sun, 28 Dec 2025 22:06:58 +1100 Subject: [PATCH 2/9] try refactor arena --- c++/src/capnp/arena.c++ | 13 +++++++++++-- c++/src/capnp/message.c++ | 16 +++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/c++/src/capnp/arena.c++ b/c++/src/capnp/arena.c++ index 400ffe1577..7def1a6dcd 100644 --- a/c++/src/capnp/arena.c++ +++ b/c++/src/capnp/arena.c++ @@ -238,7 +238,11 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { kj::ctor(segment0, this, SegmentId(0), ptr.begin(), actualSize, &this->dummyLimiter, ZERO * WORDS, dirty); segmentWithSpace = &segment0; - return AllocateResult { &segment0, segment0.allocate(amount) }; + + word* resultPtr = segment0.allocate(amount); + // Lazy zeroing: zero the first word if the memory is dirty. + if (dirty) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); + return AllocateResult { &segment0, resultPtr }; } else { if (segmentWithSpace != nullptr) { // Check if there is space in an existing segment. @@ -250,6 +254,8 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { // and shove them to the back of the queue if they have become too small. word* attempt = segmentWithSpace->allocate(amount); if (attempt != nullptr) { + // // Lazy zeroing: zero the first word if the memory is dirty. + // if (!message->isAllocationZeroed()) memset(attempt, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); return AllocateResult { segmentWithSpace, attempt }; } } @@ -262,7 +268,10 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { segmentWithSpace = result; // Allocating from the new segment is guaranteed to succeed since we made it big enough. - return AllocateResult { result, result->allocate(amount) }; + word* resultPtr = result->allocate(amount); + // Lazy zeroing: zero the first word if the memory is dirty. + if (dirty) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); + return AllocateResult { result, resultPtr }; } } diff --git a/c++/src/capnp/message.c++ b/c++/src/capnp/message.c++ index 077b62d6de..7e5133ffe4 100644 --- a/c++/src/capnp/message.c++ +++ b/c++/src/capnp/message.c++ @@ -220,13 +220,19 @@ MallocMessageBuilder::MallocMessageBuilder( ownFirstSegment(false), returnedFirstSegment(false), firstSegment(firstSegment.begin()) { KJ_REQUIRE(firstSegment.size() > 0, "First segment size must be non-zero."); - if (initStrategy == InitializationStrategy::NO_ZERO_MEMORY) { - memset(firstSegment.begin(), 0, sizeof(word)); + if (initStrategy == InitializationStrategy::ZERO_MEMORY) { + // Checking just the first word should catch most cases of failing to zero the segment. + KJ_REQUIRE(*reinterpret_cast(firstSegment.begin()) == 0, + "First segment must be zeroed."); } - // Checking just the first word should catch most cases of failing to zero the segment. - KJ_REQUIRE(*reinterpret_cast(firstSegment.begin()) == 0, - "First segment must be zeroed."); + // if (initStrategy == InitializationStrategy::NO_ZERO_MEMORY) { + // memset(firstSegment.begin(), 0, sizeof(word)); + // } + // + // // Checking just the first word should catch most cases of failing to zero the segment. + // KJ_REQUIRE(*reinterpret_cast(firstSegment.begin()) == 0, + // "First segment must be zeroed."); } // MallocMessageBuilder::MallocMessageBuilder( From edcbd4189aabe9bb9dedcbde3c9cd24a1e83e2ba Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Sun, 28 Dec 2025 22:54:50 +1100 Subject: [PATCH 3/9] bugfix tc --- c++/src/capnp/message-test.c++ | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/c++/src/capnp/message-test.c++ b/c++/src/capnp/message-test.c++ index f47c6d8de1..61f8e840f4 100644 --- a/c++/src/capnp/message-test.c++ +++ b/c++/src/capnp/message-test.c++ @@ -330,14 +330,16 @@ KJ_TEST("Corner Case: Uninitialized DATA with 1 byte padding") { // Helper class for the Custom Builder test. // Simulates a user-defined MessageBuilder that uses a custom allocator (e.g., malloc) -// and does not zero memory, overriding isAllocationZeroed() to return false. +// without zero-initializing memory. It overrides isAllocationZeroed() to return false, +// ensuring that Cap'n Proto correctly sanitizes memory where necessary (e.g., Structs, padding) +// while preserving performance optimizations (e.g., uninitialized Data). class DirtyMallocMessageBuilder : public MessageBuilder { public: DirtyMallocMessageBuilder() - : MessageBuilder(kj::heapArray({ - // Initialize with nullptr. The Arena will call allocateSegment() on first use. - SegmentInit { nullptr, 0, false } - })) {} + // Initialize with an empty segment list. This forces the Builder to call + // allocateSegment() immediately when the first object is allocated, ensuring + // we get our "dirty" memory from the start rather than assuming Segment 0 exists. + : MessageBuilder() {} ~DirtyMallocMessageBuilder() { for (void* ptr : allocations) { @@ -345,7 +347,8 @@ public: } } - // CRITICAL: Explicitly declare that this allocator provides dirty memory. + // Explicitly declare that this allocator provides dirty memory. + // This forces the MessageBuilder to proactively zero-out Structs upon initialization. bool isAllocationZeroed() const override { return false; } kj::ArrayPtr allocateSegment(uint minimumSize) override { @@ -353,7 +356,9 @@ public: void* ptr = malloc(sizeBytes); KJ_ASSERT(ptr != nullptr); - // Deliberately fill with garbage (0xEE) to verify lazy zeroing logic. + // Deliberately fill with garbage pattern (0xEE) to verify that Cap'n Proto + // correctly zeros out fields and padding, but leaves "uninitialized" data + // bodies untouched as an optimization. memset(ptr, 0xEE, sizeBytes); allocations.add(ptr); @@ -367,22 +372,26 @@ private: KJ_TEST("CustomMessageBuilder: Lazy zeroing works with custom dirty allocator") { DirtyMallocMessageBuilder builder; - // 1. Verify Safety: Structs must be zeroed. + // 1. Verify Safety: Structs must be zero-initialized. + // Even though the underlying memory is filled with 0xEE, the Builder must clear it. auto root = builder.initRoot(); KJ_EXPECT(root.getInt64Field() == 0); KJ_EXPECT(root.getBoolField() == false); // 2. Verify Performance & Security for Data Blob. - // Allocate 3 bytes. Expect 0xEE in body, 0x00 in padding. + // Allocate 3 bytes of Data. + // Memory Layout (1 word): [Body: 0, 1, 2] [Padding: 3, 4, 5, 6, 7] auto data = root.initDataFieldUninitialized(3); const byte* ptr = data.begin(); - // Check Body (Dirty / Performance) + // Check Body: Should remain dirty (0xEE) because we requested "Uninitialized". + // This confirms the performance optimization (skipping memset on the body). KJ_EXPECT(ptr[0] == 0xEE); KJ_EXPECT(ptr[1] == 0xEE); KJ_EXPECT(ptr[2] == 0xEE); - // Check Padding (Clean / Security) + // Check Padding: Must be zeroed (0x00). + // This confirms security (prevention of information leaks in padding bytes). KJ_EXPECT(ptr[3] == 0x00); KJ_EXPECT(ptr[4] == 0x00); KJ_EXPECT(ptr[5] == 0x00); From 67c095c06219d4f594a38c75a14ddb6cf58fb784 Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Mon, 29 Dec 2025 15:10:28 +1100 Subject: [PATCH 4/9] temp refine --- c++/src/capnp/arena.c++ | 25 +++++++++----------- c++/src/capnp/arena.h | 18 +++++++------- c++/src/capnp/compiler/capnpc-c++.c++ | 6 ++--- c++/src/capnp/layout.c++ | 23 +++++++++--------- c++/src/capnp/layout.h | 4 ++-- c++/src/capnp/message-test.c++ | 8 +++---- c++/src/capnp/message.c++ | 34 ++------------------------- c++/src/capnp/message.h | 10 ++++---- 8 files changed, 47 insertions(+), 81 deletions(-) diff --git a/c++/src/capnp/arena.c++ b/c++/src/capnp/arena.c++ index 7def1a6dcd..bbcff7740b 100644 --- a/c++/src/capnp/arena.c++ +++ b/c++/src/capnp/arena.c++ @@ -229,19 +229,18 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { kj::ArrayPtr ptr = message->allocateSegment(unbound(amount / WORDS)); auto actualSize = verifySegment(ptr); - // Check dirtiness - bool dirty = !message->isAllocationZeroed(); + // Check if memory is pre-zeroed + bool notZeroed = !message->isAllocationZeroed(); // Re-allocate segment0 in-place. This is a bit of a hack, but we have not returned any // pointers to this segment yet, so it should be fine. kj::dtor(segment0); - kj::ctor(segment0, this, SegmentId(0), ptr.begin(), actualSize, &this->dummyLimiter, ZERO * WORDS, dirty); + kj::ctor(segment0, this, SegmentId(0), ptr.begin(), actualSize, &this->dummyLimiter, ZERO * WORDS, notZeroed); segmentWithSpace = &segment0; - word* resultPtr = segment0.allocate(amount); - // Lazy zeroing: zero the first word if the memory is dirty. - if (dirty) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); + // Zero the root pointer field if the memory is not pre-zeroed. + if (notZeroed) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); return AllocateResult { &segment0, resultPtr }; } else { if (segmentWithSpace != nullptr) { @@ -254,23 +253,21 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { // and shove them to the back of the queue if they have become too small. word* attempt = segmentWithSpace->allocate(amount); if (attempt != nullptr) { - // // Lazy zeroing: zero the first word if the memory is dirty. - // if (!message->isAllocationZeroed()) memset(attempt, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); return AllocateResult { segmentWithSpace, attempt }; } } // Need to allocate a new segment. - bool dirty = !message->isAllocationZeroed(); - SegmentBuilder* result = addSegmentInternal(message->allocateSegment(unbound(amount / WORDS)), dirty); + bool notZeroed = !message->isAllocationZeroed(); + SegmentBuilder* result = addSegmentInternal(message->allocateSegment(unbound(amount / WORDS)), notZeroed); // Check this new segment first the next time we need to allocate. segmentWithSpace = result; // Allocating from the new segment is guaranteed to succeed since we made it big enough. word* resultPtr = result->allocate(amount); - // Lazy zeroing: zero the first word if the memory is dirty. - if (dirty) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); + // Zero the root pointer field if the memory is not pre-zeroed. + if (notZeroed) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); return AllocateResult { result, resultPtr }; } } @@ -280,7 +277,7 @@ SegmentBuilder* BuilderArena::addExternalSegment(kj::ArrayPtr conten } template -SegmentBuilder* BuilderArena::addSegmentInternal(kj::ArrayPtr content, bool possiblyDirty) { +SegmentBuilder* BuilderArena::addSegmentInternal(kj::ArrayPtr content, bool notZeroed) { // This check should never fail in practice, since you can't get an Orphanage without allocating // the root segment. KJ_REQUIRE(segment0.getArena() != nullptr, @@ -299,7 +296,7 @@ SegmentBuilder* BuilderArena::addSegmentInternal(kj::ArrayPtr content, bool p kj::Own newBuilder = kj::heap( this, SegmentId(segmentState->builders.size() + 1), - content.begin(), contentSize, &this->dummyLimiter, ZERO * WORDS, possiblyDirty); + content.begin(), contentSize, &this->dummyLimiter, ZERO * WORDS, notZeroed); SegmentBuilder* result = newBuilder.get(); segmentState->builders.add(kj::mv(newBuilder)); diff --git a/c++/src/capnp/arena.h b/c++/src/capnp/arena.h index 63f5d1a201..37cbd364c5 100644 --- a/c++/src/capnp/arena.h +++ b/c++/src/capnp/arena.h @@ -186,10 +186,10 @@ class SegmentBuilder: public SegmentReader { public: inline SegmentBuilder(BuilderArena* arena, SegmentId id, word* ptr, SegmentWordCount size, ReadLimiter* readLimiter, SegmentWordCount wordsUsed = ZERO * WORDS, - bool possiblyDirty = false); + bool notZeroed = false); inline SegmentBuilder(BuilderArena* arena, SegmentId id, const word* ptr, SegmentWordCount size, ReadLimiter* readLimiter, SegmentWordCount wordsUsed = ZERO * WORDS, - bool possiblyDirty = false); + bool notZeroed = false); inline SegmentBuilder(BuilderArena* arena, SegmentId id, decltype(nullptr), ReadLimiter* readLimiter); @@ -210,7 +210,7 @@ class SegmentBuilder: public SegmentReader { inline bool isWritable() { return !readOnly; } - inline bool isPossiblyDirty() { return possiblyDirty; } + inline bool isNotZeroed() { return notZeroed; } inline void tryTruncate(word* from, word* to); // If `from` points just past the current end of the segment, then move the end back to `to`. @@ -228,7 +228,7 @@ class SegmentBuilder: public SegmentReader { bool readOnly; - bool possiblyDirty; + bool notZeroed; [[noreturn]] void throwNotWritable(); @@ -382,7 +382,7 @@ class BuilderArena final: public Arena { // segment that is already-full, in which case we don't update this pointer. template // Can be `word` or `const word`. - SegmentBuilder* addSegmentInternal(kj::ArrayPtr content, bool possiblyDirty = false); + SegmentBuilder* addSegmentInternal(kj::ArrayPtr content, bool notZeroed = false); }; // ======================================================================================= @@ -458,16 +458,16 @@ inline void SegmentReader::unread(WordCount64 amount) { readLimiter->unread(amou inline SegmentBuilder::SegmentBuilder( BuilderArena* arena, SegmentId id, word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter, SegmentWordCount wordsUsed, bool possiblyDirty) + ReadLimiter* readLimiter, SegmentWordCount wordsUsed, bool notZeroed) : SegmentReader(arena, id, ptr, size, readLimiter), - pos(ptr + wordsUsed), readOnly(false), possiblyDirty(possiblyDirty) {} + pos(ptr + wordsUsed), readOnly(false), notZeroed(notZeroed) {} inline SegmentBuilder::SegmentBuilder( BuilderArena* arena, SegmentId id, const word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter, SegmentWordCount /*wordsUsed*/, bool possiblyDirty) + ReadLimiter* readLimiter, SegmentWordCount /*wordsUsed*/, bool notZeroed) : SegmentReader(arena, id, ptr, size, readLimiter), // const_cast is safe here because the member won't ever be dereferenced because it appears // to point to the end of the segment anyway. - pos(const_cast(ptr + size)), readOnly(true), possiblyDirty(possiblyDirty) {} + pos(const_cast(ptr + size)), readOnly(true), notZeroed(notZeroed) {} inline SegmentBuilder::SegmentBuilder(BuilderArena* arena, SegmentId id, decltype(nullptr), ReadLimiter* readLimiter) : SegmentReader(arena, id, nullptr, ZERO * WORDS, readLimiter), diff --git a/c++/src/capnp/compiler/capnpc-c++.c++ b/c++/src/capnp/compiler/capnpc-c++.c++ index 3661acb4b9..9798c061c7 100644 --- a/c++/src/capnp/compiler/capnpc-c++.c++ +++ b/c++/src/capnp/compiler/capnpc-c++.c++ @@ -1757,7 +1757,7 @@ private: COND(!shouldTemplatizeInit, " ", maybeInline, builderType, " init", titleCase, "(unsigned int size);\n")), COND(shouldIncludeSizedInit && !shouldTemplatizeInit && typeSchema.which() == schema::Type::DATA, - " ", maybeInline, builderType, " init", titleCase, "Uninitialized(unsigned int size);\n"), + " ", maybeInline, builderType, " uninitialized", titleCase, "(unsigned int size);\n"), " ", maybeInline, "void adopt", titleCase, "(::capnp::Orphan<", type, ">&& value);\n" " ", maybeInline, "::capnp::Orphan<", type, "> disown", titleCase, "();\n", COND(shouldExcludeInLiteMode, "#endif // !CAPNP_LITE\n"), @@ -1830,10 +1830,10 @@ private: "}\n"), COND(shouldIncludeSizedInit && !shouldTemplatizeInit && typeSchema.which() == schema::Type::DATA, templateContext.allDecls(), - maybeInline, builderType, " ", scope, "Builder::init", titleCase, "Uninitialized(unsigned int size) {\n", + maybeInline, builderType, " ", scope, "Builder::uninitialized", titleCase, "(unsigned int size) {\n", unionDiscrim.set, " return _builder.getPointerField(\n" - " ::capnp::bounded<", offset, ">() * ::capnp::POINTERS).initBlobUninitialized<", type, ">(size);\n" + " ::capnp::bounded<", offset, ">() * ::capnp::POINTERS).uninitializedBlob<", type, ">(size);\n" "}\n"), templateContext.allDecls(), maybeInline, "void ", scope, "Builder::adopt", titleCase, "(\n" diff --git a/c++/src/capnp/layout.c++ b/c++/src/capnp/layout.c++ index 7d5f60a033..dd622631cd 100644 --- a/c++/src/capnp/layout.c++ +++ b/c++/src/capnp/layout.c++ @@ -515,9 +515,8 @@ struct WireHelpers { ref = reinterpret_cast(ptr); ref->setKindAndTarget(kind, ptr + POINTER_SIZE_IN_WORDS, segment); - // 如果需要清零,并且 segment 是 dirty 的,则清零 landing pad 之后的数据部分 - // Landing pad (ptr) 已经被 setKindAndTarget 写过了,不需要清零 - if (zeroMemory && segment->isPossiblyDirty()) { + // If zeroing is required and the segment is not pre-zeroed, zero the data portion after the landing pad. + if (zeroMemory && segment->isNotZeroed()) { WireHelpers::zeroMemory(ptr + POINTER_SIZE_IN_WORDS, amount); } @@ -526,8 +525,8 @@ struct WireHelpers { } else { ref->setKindAndTarget(kind, ptr, segment); - // 如果需要清零且 segment 可能是脏的 - if (zeroMemory && segment->isPossiblyDirty()) { + // If zeroing is required and the segment is not pre-zeroed. + if (zeroMemory && segment->isNotZeroed()) { WireHelpers::zeroMemory(ptr, amount); } return ptr; @@ -538,8 +537,8 @@ struct WireHelpers { auto allocation = orphanArena->allocate(amount); segment = allocation.segment; ref->setKindForOrphan(kind); - // OrphanArena 分配的也需要检查 dirty - if (zeroMemory && segment->isPossiblyDirty()) { + // Check for not zeroed memory allocated by OrphanArena. + if (zeroMemory && segment->isNotZeroed()) { WireHelpers::zeroMemory(allocation.words, amount); } return allocation.words; @@ -1732,10 +1731,10 @@ struct WireHelpers { return { segment, Data::Builder(reinterpret_cast(ptr), unbound(size / BYTES)) }; } - static KJ_ALWAYS_INLINE(SegmentAnd initDataPointerUninitialized( + static KJ_ALWAYS_INLINE(SegmentAnd uninitializedDataPointer( WirePointer* ref, SegmentBuilder* segment, CapTableBuilder* capTable, BlobSize size, BuilderArena* orphanArena = nullptr)) { - // Allocate with zeroMemory = false for performance. + // Allocate the space with zeroMemory = false word* ptr = allocate(ref, segment, capTable, roundBytesUpToWords(size), WirePointer::LIST, orphanArena, false); @@ -1743,7 +1742,7 @@ struct WireHelpers { ref->listRef.set(ElementSize::BYTE, size * (ONE * ELEMENTS / BYTES)); // Security: Zero-out padding bytes if memory is dirty. - if (segment->isPossiblyDirty()) { + if (segment->isNotZeroed()) { size_t byteSize = unbound(size / BYTES); // Calculate the actual allocated size in bytes (aligned to Word boundary). size_t allocatedSize = unbound(roundBytesUpToWords(size)) * sizeof(word); @@ -2636,8 +2635,8 @@ Data::Builder PointerBuilder::initBlob(ByteCount size) { assertMaxBits(size, ThrowOverflow())).value; } template <> -Data::Builder PointerBuilder::initBlobUninitialized(ByteCount size) { - return WireHelpers::initDataPointerUninitialized(pointer, segment, capTable, +Data::Builder PointerBuilder::uninitializedBlob(ByteCount size) { + return WireHelpers::uninitializedDataPointer(pointer, segment, capTable, assertMaxBits(size, ThrowOverflow())).value; } template <> diff --git a/c++/src/capnp/layout.h b/c++/src/capnp/layout.h index 1defc35722..240b1d61b6 100644 --- a/c++/src/capnp/layout.h +++ b/c++/src/capnp/layout.h @@ -358,7 +358,7 @@ class PointerBuilder: public kj::DisallowConstCopy { ListBuilder initList(ElementSize elementSize, ElementCount elementCount); ListBuilder initStructList(ElementCount elementCount, StructSize size); template typename T::Builder initBlob(ByteCount size); - template typename T::Builder initBlobUninitialized(ByteCount size); + template typename T::Builder uninitializedBlob(ByteCount size); // Init methods: Initialize the pointer to a newly-allocated object, discarding the existing // object. @@ -968,7 +968,7 @@ template <> typename Text::Reader PointerReader::getBlob( const void* defaultValue, ByteCount defaultSize) const; template <> typename Data::Builder PointerBuilder::initBlob(ByteCount size); -template <> typename Data::Builder PointerBuilder::initBlobUninitialized(ByteCount size); +template <> typename Data::Builder PointerBuilder::uninitializedBlob(ByteCount size); template <> void PointerBuilder::setBlob(typename Data::Reader value); template <> typename Data::Builder PointerBuilder::getBlob( const void* defaultValue, ByteCount defaultSize); diff --git a/c++/src/capnp/message-test.c++ b/c++/src/capnp/message-test.c++ index 61f8e840f4..2ece583333 100644 --- a/c++/src/capnp/message-test.c++ +++ b/c++/src/capnp/message-test.c++ @@ -240,7 +240,7 @@ KJ_TEST("SECURITY: Uninitialized DATA zeroes padding bytes (Info Leak Prevention // Physical layout: 1 word (8 bytes) allocated. // Bytes [0, 1, 2] are the data body. // Bytes [3, 4, 5, 6, 7] are alignment padding. - auto data = root.initDataFieldUninitialized(3); + auto data = root.uninitializedDataField(3); const byte* rawPtr = data.begin(); // CHECK 1: Performance verification. @@ -292,7 +292,7 @@ KJ_TEST("Corner Case: Uninitialized DATA with exact word alignment") { // 8 bytes -> Exactly 1 word. Padding size is 0. // The padding zeroing logic must not crash or overwrite the next word. - auto data = root.initDataFieldUninitialized(8); + auto data = root.uninitializedDataField(8); const byte* rawPtr = data.begin(); // Body is dirty (0xCC). @@ -316,7 +316,7 @@ KJ_TEST("Corner Case: Uninitialized DATA with 1 byte padding") { MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); auto root = builder.initRoot(); - auto data = root.initDataFieldUninitialized(7); + auto data = root.uninitializedDataField(7); const byte* rawPtr = data.begin(); // Body [0..6] is dirty. @@ -381,7 +381,7 @@ KJ_TEST("CustomMessageBuilder: Lazy zeroing works with custom dirty allocator") // 2. Verify Performance & Security for Data Blob. // Allocate 3 bytes of Data. // Memory Layout (1 word): [Body: 0, 1, 2] [Padding: 3, 4, 5, 6, 7] - auto data = root.initDataFieldUninitialized(3); + auto data = root.uninitializedDataField(3); const byte* ptr = data.begin(); // Check Body: Should remain dirty (0xEE) because we requested "Uninitialized". diff --git a/c++/src/capnp/message.c++ b/c++/src/capnp/message.c++ index 7e5133ffe4..dddafdae57 100644 --- a/c++/src/capnp/message.c++ +++ b/c++/src/capnp/message.c++ @@ -220,43 +220,13 @@ MallocMessageBuilder::MallocMessageBuilder( ownFirstSegment(false), returnedFirstSegment(false), firstSegment(firstSegment.begin()) { KJ_REQUIRE(firstSegment.size() > 0, "First segment size must be non-zero."); - if (initStrategy == InitializationStrategy::ZERO_MEMORY) { + if (initStrategy == InitializationStrategy::PRE_ZERO_MEMORY) { // Checking just the first word should catch most cases of failing to zero the segment. KJ_REQUIRE(*reinterpret_cast(firstSegment.begin()) == 0, "First segment must be zeroed."); } - - // if (initStrategy == InitializationStrategy::NO_ZERO_MEMORY) { - // memset(firstSegment.begin(), 0, sizeof(word)); - // } - // - // // Checking just the first word should catch most cases of failing to zero the segment. - // KJ_REQUIRE(*reinterpret_cast(firstSegment.begin()) == 0, - // "First segment must be zeroed."); } -// MallocMessageBuilder::MallocMessageBuilder( -// kj::ArrayPtr firstSegment, AllocationStrategy allocationStrategy, InitializationStrategy initStrategy) -// : MessageBuilder(kj::heapArray({ -// SegmentInit { -// firstSegment, -// 0, -// initStrategy == InitializationStrategy::ZERO_MEMORY -// } -// })), -// nextSize(firstSegment.size()), allocationStrategy(allocationStrategy), -// initializationStrategy(initStrategy), -// ownFirstSegment(false), returnedFirstSegment(false), firstSegment(firstSegment.begin()) { -// -// KJ_REQUIRE(firstSegment.size() > 0, "First segment size must be non-zero."); -// -// if (initStrategy == InitializationStrategy::ZERO_MEMORY) { -// // Checking just the first word should catch most cases of failing to zero the segment. -// KJ_REQUIRE(*reinterpret_cast(firstSegment.begin()) == 0, -// "First segment must be zeroed."); -// } -// } - MallocMessageBuilder::~MallocMessageBuilder() noexcept(false) { if (returnedFirstSegment) { if (ownFirstSegment) { @@ -299,7 +269,7 @@ kj::ArrayPtr MallocMessageBuilder::allocateSegment(uint minimumSize) { void* result; // 根据策略选择分配方式 - if (initializationStrategy == InitializationStrategy::ZERO_MEMORY) { + if (initializationStrategy == InitializationStrategy::PRE_ZERO_MEMORY) { result = calloc(size, sizeof(word)); if (result == nullptr) { KJ_FAIL_SYSCALL("calloc(size, sizeof(word))", ENOMEM, size); diff --git a/c++/src/capnp/message.h b/c++/src/capnp/message.h index 3f2535590f..722025eeee 100644 --- a/c++/src/capnp/message.h +++ b/c++/src/capnp/message.h @@ -383,13 +383,13 @@ class MallocMessageBuilder: public MessageBuilder { public: enum class InitializationStrategy: uint8_t { - ZERO_MEMORY, - NO_ZERO_MEMORY + PRE_ZERO_MEMORY, + LAZY_ZERO_MEMORY }; explicit MallocMessageBuilder(uint firstSegmentWords = SUGGESTED_FIRST_SEGMENT_WORDS, AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY, - InitializationStrategy initStrategy = InitializationStrategy::ZERO_MEMORY); + InitializationStrategy initStrategy = InitializationStrategy::PRE_ZERO_MEMORY); // Creates a BuilderContext which allocates at least the given number of words for the first // segment, and then uses the given strategy to decide how much to allocate for subsequent // segments. When choosing a value for firstSegmentWords, consider that: @@ -404,7 +404,7 @@ class MallocMessageBuilder: public MessageBuilder { explicit MallocMessageBuilder(kj::ArrayPtr firstSegment, AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY, - InitializationStrategy initStrategy = InitializationStrategy::ZERO_MEMORY); + InitializationStrategy initStrategy = InitializationStrategy::PRE_ZERO_MEMORY); // This version always returns the given array for the first segment, and then proceeds with the // allocation strategy. This is useful for optimization when building lots of small messages in // a tight loop: you can reuse the space for the first segment. @@ -413,7 +413,7 @@ class MallocMessageBuilder: public MessageBuilder { // over any space that was used so that it can be reused. virtual bool isAllocationZeroed() const override { - return initializationStrategy == InitializationStrategy::ZERO_MEMORY; + return initializationStrategy == InitializationStrategy::PRE_ZERO_MEMORY; } KJ_DISALLOW_COPY_AND_MOVE(MallocMessageBuilder); From 8d45e0db821a712342cf713871e20a144f9cf17f Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Mon, 29 Dec 2025 15:22:35 +1100 Subject: [PATCH 5/9] bugfix --- c++/src/capnp/message-test.c++ | 16 ++++++++-------- c++/src/capnp/message.c++ | 26 ++++++++++++++------------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/c++/src/capnp/message-test.c++ b/c++/src/capnp/message-test.c++ index 2ece583333..362993e28f 100644 --- a/c++/src/capnp/message-test.c++ +++ b/c++/src/capnp/message-test.c++ @@ -214,10 +214,10 @@ KJ_TEST("MessageBuilder::sizeInWords()") { KJ_EXPECT(reader.sizeInWords() == expected); } -KJ_TEST("MallocMessageBuilder with NO_ZERO_MEMORY strategy") { +KJ_TEST("MallocMessageBuilder with LAZY_ZERO_MEMORY strategy") { // Verify that the new InitializationStrategy API works for basic usage. MallocMessageBuilder builder(1024, AllocationStrategy::FIXED_SIZE, - MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + MallocMessageBuilder::InitializationStrategy::LAZY_ZERO_MEMORY); auto root = builder.initRoot(); root.setInt64Field(12345); KJ_EXPECT(root.getInt64Field() == 12345); @@ -232,7 +232,7 @@ KJ_TEST("SECURITY: Uninitialized DATA zeroes padding bytes (Info Leak Prevention // Create a builder that opts out of zeroing. MallocMessageBuilder builder(scratch, AllocationStrategy::FIXED_SIZE, - MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + MallocMessageBuilder::InitializationStrategy::LAZY_ZERO_MEMORY); auto root = builder.initRoot(); @@ -260,17 +260,17 @@ KJ_TEST("SECURITY: Uninitialized DATA zeroes padding bytes (Info Leak Prevention KJ_EXPECT(rawPtr[7] == 0x00); } -KJ_TEST("SAFETY: Structs are always zeroed even with NO_ZERO_MEMORY") { +KJ_TEST("SAFETY: Structs are always zeroed even with LAZY_ZERO_MEMORY") { // Setup a dirty buffer (filled with 0xBB). byte dirtyBuffer[1024]; memset(dirtyBuffer, 0xBB, sizeof(dirtyBuffer)); kj::ArrayPtr scratch(reinterpret_cast(dirtyBuffer), sizeof(dirtyBuffer) / sizeof(word)); MallocMessageBuilder builder(scratch, AllocationStrategy::FIXED_SIZE, - MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + MallocMessageBuilder::InitializationStrategy::LAZY_ZERO_MEMORY); // Initialize a Struct. - // Even though the builder is in NO_ZERO_MEMORY mode, Struct allocations must always + // Even though the builder is in LAZY_ZERO_MEMORY mode, Struct allocations must always // enforce zero-initialization to ensure pointer validity and default values. auto root = builder.initRoot(); @@ -287,7 +287,7 @@ KJ_TEST("Corner Case: Uninitialized DATA with exact word alignment") { kj::ArrayPtr scratch(reinterpret_cast(dirtyBuffer), sizeof(dirtyBuffer) / sizeof(word)); MallocMessageBuilder builder(scratch, AllocationStrategy::FIXED_SIZE, - MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + MallocMessageBuilder::InitializationStrategy::LAZY_ZERO_MEMORY); auto root = builder.initRoot(); // 8 bytes -> Exactly 1 word. Padding size is 0. @@ -313,7 +313,7 @@ KJ_TEST("Corner Case: Uninitialized DATA with 1 byte padding") { kj::ArrayPtr scratch(reinterpret_cast(dirtyBuffer), sizeof(dirtyBuffer) / sizeof(word)); MallocMessageBuilder builder(scratch, AllocationStrategy::FIXED_SIZE, - MallocMessageBuilder::InitializationStrategy::NO_ZERO_MEMORY); + MallocMessageBuilder::InitializationStrategy::LAZY_ZERO_MEMORY); auto root = builder.initRoot(); auto data = root.uninitializedDataField(7); diff --git a/c++/src/capnp/message.c++ b/c++/src/capnp/message.c++ index dddafdae57..1020e5f001 100644 --- a/c++/src/capnp/message.c++ +++ b/c++/src/capnp/message.c++ @@ -267,19 +267,21 @@ kj::ArrayPtr MallocMessageBuilder::allocateSegment(uint minimumSize) { uint size = kj::max(minimumSize, nextSize); + // Allocate memory based on strategy void* result; - // 根据策略选择分配方式 - if (initializationStrategy == InitializationStrategy::PRE_ZERO_MEMORY) { - result = calloc(size, sizeof(word)); - if (result == nullptr) { - KJ_FAIL_SYSCALL("calloc(size, sizeof(word))", ENOMEM, size); - } - } else { - // NO_ZERO_MEMORY: 使用 malloc,不进行清零 - result = malloc(size * sizeof(word)); - if (result == nullptr) { - KJ_FAIL_SYSCALL("malloc(size * sizeof(word))", ENOMEM, size); - } + switch (initializationStrategy) { + case InitializationStrategy::PRE_ZERO_MEMORY: + result = calloc(size, sizeof(word)); + break; + case InitializationStrategy::LAZY_ZERO_MEMORY: + result = malloc(size * sizeof(word)); + break; + default: + KJ_FAIL_ASSERT("Unknown initialization strategy"); + } + + if (result == nullptr) { + KJ_FAIL_SYSCALL("memory allocation failed", ENOMEM, size); } if (!returnedFirstSegment) { From d478c40d7f66d5387fd6f7869eb7994df26990f9 Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Mon, 29 Dec 2025 16:20:22 +1100 Subject: [PATCH 6/9] temp refine --- c++/src/capnp/arena.c++ | 22 ++++++++++++---------- c++/src/capnp/arena.h | 2 ++ c++/src/capnp/message-test.c++ | 2 +- c++/src/capnp/message.h | 6 +++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/c++/src/capnp/arena.c++ b/c++/src/capnp/arena.c++ index bbcff7740b..d64bba49a2 100644 --- a/c++/src/capnp/arena.c++ +++ b/c++/src/capnp/arena.c++ @@ -158,14 +158,16 @@ void ReaderArena::reportReadLimitReached() { // ======================================================================================= BuilderArena::BuilderArena(MessageBuilder* message) - : message(message), segment0(nullptr, SegmentId(0), nullptr, nullptr) {} + : message(message), segment0(nullptr, SegmentId(0), nullptr, nullptr), + needLazyZero(message->needLazyZero()) {} BuilderArena::BuilderArena(MessageBuilder* message, kj::ArrayPtr segments) : message(message), segment0(this, SegmentId(0), segments[0].space.begin(), verifySegment(segments[0].space), - &this->dummyLimiter, verifySegmentSize(segments[0].wordsUsed), !segments[0].isZeroed) { + &this->dummyLimiter, verifySegmentSize(segments[0].wordsUsed), !segments[0].isZeroed), + needLazyZero(message->needLazyZero()){ if (segments.size() > 1) { kj::Vector> builders(segments.size() - 1); @@ -224,23 +226,23 @@ SegmentBuilder* BuilderArena::getSegment(SegmentId id) { } BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { + // Check if memory needs lazy zero + bool needLazyZero = this->needLazyZero; + if (segment0.getArena() == nullptr) { // We're allocating the first segment. kj::ArrayPtr ptr = message->allocateSegment(unbound(amount / WORDS)); auto actualSize = verifySegment(ptr); - // Check if memory is pre-zeroed - bool notZeroed = !message->isAllocationZeroed(); - // Re-allocate segment0 in-place. This is a bit of a hack, but we have not returned any // pointers to this segment yet, so it should be fine. kj::dtor(segment0); - kj::ctor(segment0, this, SegmentId(0), ptr.begin(), actualSize, &this->dummyLimiter, ZERO * WORDS, notZeroed); + kj::ctor(segment0, this, SegmentId(0), ptr.begin(), actualSize, &this->dummyLimiter, ZERO * WORDS, needLazyZero); segmentWithSpace = &segment0; word* resultPtr = segment0.allocate(amount); // Zero the root pointer field if the memory is not pre-zeroed. - if (notZeroed) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); + if (needLazyZero) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); return AllocateResult { &segment0, resultPtr }; } else { if (segmentWithSpace != nullptr) { @@ -258,8 +260,8 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { } // Need to allocate a new segment. - bool notZeroed = !message->isAllocationZeroed(); - SegmentBuilder* result = addSegmentInternal(message->allocateSegment(unbound(amount / WORDS)), notZeroed); + SegmentBuilder* result = addSegmentInternal(message->allocateSegment(unbound(amount / WORDS)), + needLazyZero); // Check this new segment first the next time we need to allocate. segmentWithSpace = result; @@ -267,7 +269,7 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { // Allocating from the new segment is guaranteed to succeed since we made it big enough. word* resultPtr = result->allocate(amount); // Zero the root pointer field if the memory is not pre-zeroed. - if (notZeroed) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); + if (needLazyZero) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); return AllocateResult { result, resultPtr }; } } diff --git a/c++/src/capnp/arena.h b/c++/src/capnp/arena.h index 37cbd364c5..f98ecc97a1 100644 --- a/c++/src/capnp/arena.h +++ b/c++/src/capnp/arena.h @@ -383,6 +383,8 @@ class BuilderArena final: public Arena { template // Can be `word` or `const word`. SegmentBuilder* addSegmentInternal(kj::ArrayPtr content, bool notZeroed = false); + + bool needLazyZero = false; }; // ======================================================================================= diff --git a/c++/src/capnp/message-test.c++ b/c++/src/capnp/message-test.c++ index 362993e28f..dfc0892e0a 100644 --- a/c++/src/capnp/message-test.c++ +++ b/c++/src/capnp/message-test.c++ @@ -349,7 +349,7 @@ public: // Explicitly declare that this allocator provides dirty memory. // This forces the MessageBuilder to proactively zero-out Structs upon initialization. - bool isAllocationZeroed() const override { return false; } + bool needLazyZero() const override { return true; } kj::ArrayPtr allocateSegment(uint minimumSize) override { size_t sizeBytes = minimumSize * sizeof(word); diff --git a/c++/src/capnp/message.h b/c++/src/capnp/message.h index 722025eeee..c1806fedad 100644 --- a/c++/src/capnp/message.h +++ b/c++/src/capnp/message.h @@ -204,7 +204,7 @@ class MessageBuilder { // because otherwise the Cap'n Proto implementation would have to zero the memory anyway, and // many allocators are able to provide already-zero'd memory more efficiently. - virtual bool isAllocationZeroed() const { return true; } + virtual bool needLazyZero() const { return false; } template typename RootType::Builder initRoot(); @@ -412,8 +412,8 @@ class MallocMessageBuilder: public MessageBuilder { // firstSegment MUST be zero-initialized. MallocMessageBuilder's destructor will write new zeros // over any space that was used so that it can be reused. - virtual bool isAllocationZeroed() const override { - return initializationStrategy == InitializationStrategy::PRE_ZERO_MEMORY; + bool needLazyZero() const override { + return initializationStrategy == InitializationStrategy::LAZY_ZERO_MEMORY; } KJ_DISALLOW_COPY_AND_MOVE(MallocMessageBuilder); From 7c8273964913a00e82b5ad3385b51c8f14f49577 Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Mon, 29 Dec 2025 16:40:27 +1100 Subject: [PATCH 7/9] temp refine --- c++/src/capnp/arena.h | 18 +++++++++--------- c++/src/capnp/layout.c++ | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/c++/src/capnp/arena.h b/c++/src/capnp/arena.h index f98ecc97a1..84e563d881 100644 --- a/c++/src/capnp/arena.h +++ b/c++/src/capnp/arena.h @@ -186,10 +186,10 @@ class SegmentBuilder: public SegmentReader { public: inline SegmentBuilder(BuilderArena* arena, SegmentId id, word* ptr, SegmentWordCount size, ReadLimiter* readLimiter, SegmentWordCount wordsUsed = ZERO * WORDS, - bool notZeroed = false); + bool needLazyZero = false); inline SegmentBuilder(BuilderArena* arena, SegmentId id, const word* ptr, SegmentWordCount size, ReadLimiter* readLimiter, SegmentWordCount wordsUsed = ZERO * WORDS, - bool notZeroed = false); + bool needLazyZero = false); inline SegmentBuilder(BuilderArena* arena, SegmentId id, decltype(nullptr), ReadLimiter* readLimiter); @@ -210,7 +210,7 @@ class SegmentBuilder: public SegmentReader { inline bool isWritable() { return !readOnly; } - inline bool isNotZeroed() { return notZeroed; } + inline bool needLazyZero() { return needLazyZero_; } inline void tryTruncate(word* from, word* to); // If `from` points just past the current end of the segment, then move the end back to `to`. @@ -228,7 +228,7 @@ class SegmentBuilder: public SegmentReader { bool readOnly; - bool notZeroed; + bool needLazyZero_; [[noreturn]] void throwNotWritable(); @@ -382,7 +382,7 @@ class BuilderArena final: public Arena { // segment that is already-full, in which case we don't update this pointer. template // Can be `word` or `const word`. - SegmentBuilder* addSegmentInternal(kj::ArrayPtr content, bool notZeroed = false); + SegmentBuilder* addSegmentInternal(kj::ArrayPtr content, bool needLazyZero = false); bool needLazyZero = false; }; @@ -460,16 +460,16 @@ inline void SegmentReader::unread(WordCount64 amount) { readLimiter->unread(amou inline SegmentBuilder::SegmentBuilder( BuilderArena* arena, SegmentId id, word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter, SegmentWordCount wordsUsed, bool notZeroed) + ReadLimiter* readLimiter, SegmentWordCount wordsUsed, bool needLazyZero) : SegmentReader(arena, id, ptr, size, readLimiter), - pos(ptr + wordsUsed), readOnly(false), notZeroed(notZeroed) {} + pos(ptr + wordsUsed), readOnly(false), needLazyZero_(needLazyZero) {} inline SegmentBuilder::SegmentBuilder( BuilderArena* arena, SegmentId id, const word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter, SegmentWordCount /*wordsUsed*/, bool notZeroed) + ReadLimiter* readLimiter, SegmentWordCount /*wordsUsed*/, bool needLazyZero) : SegmentReader(arena, id, ptr, size, readLimiter), // const_cast is safe here because the member won't ever be dereferenced because it appears // to point to the end of the segment anyway. - pos(const_cast(ptr + size)), readOnly(true), notZeroed(notZeroed) {} + pos(const_cast(ptr + size)), readOnly(true), needLazyZero_(needLazyZero) {} inline SegmentBuilder::SegmentBuilder(BuilderArena* arena, SegmentId id, decltype(nullptr), ReadLimiter* readLimiter) : SegmentReader(arena, id, nullptr, ZERO * WORDS, readLimiter), diff --git a/c++/src/capnp/layout.c++ b/c++/src/capnp/layout.c++ index dd622631cd..b347cee85e 100644 --- a/c++/src/capnp/layout.c++ +++ b/c++/src/capnp/layout.c++ @@ -515,8 +515,8 @@ struct WireHelpers { ref = reinterpret_cast(ptr); ref->setKindAndTarget(kind, ptr + POINTER_SIZE_IN_WORDS, segment); - // If zeroing is required and the segment is not pre-zeroed, zero the data portion after the landing pad. - if (zeroMemory && segment->isNotZeroed()) { + // If clean memory is wanted and the segment is not pre-zeroed, zero the data portion after the landing pad. + if (zeroMemory && segment->needLazyZero()) { WireHelpers::zeroMemory(ptr + POINTER_SIZE_IN_WORDS, amount); } @@ -525,8 +525,8 @@ struct WireHelpers { } else { ref->setKindAndTarget(kind, ptr, segment); - // If zeroing is required and the segment is not pre-zeroed. - if (zeroMemory && segment->isNotZeroed()) { + // If clean memory is wanted and the segment is not pre-zeroed, zero the data portion after the landing pad. + if (zeroMemory && segment->needLazyZero()) { WireHelpers::zeroMemory(ptr, amount); } return ptr; @@ -537,8 +537,8 @@ struct WireHelpers { auto allocation = orphanArena->allocate(amount); segment = allocation.segment; ref->setKindForOrphan(kind); - // Check for not zeroed memory allocated by OrphanArena. - if (zeroMemory && segment->isNotZeroed()) { + // Check for the need to lazy zero the segment allocated by OrphanArena. + if (zeroMemory && segment->needLazyZero()) { WireHelpers::zeroMemory(allocation.words, amount); } return allocation.words; @@ -1742,7 +1742,7 @@ struct WireHelpers { ref->listRef.set(ElementSize::BYTE, size * (ONE * ELEMENTS / BYTES)); // Security: Zero-out padding bytes if memory is dirty. - if (segment->isNotZeroed()) { + if (segment->needLazyZero()) { size_t byteSize = unbound(size / BYTES); // Calculate the actual allocated size in bytes (aligned to Word boundary). size_t allocatedSize = unbound(roundBytesUpToWords(size)) * sizeof(word); From b922883eedcb3dfe103b19d17a0a5edb67cb905f Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Tue, 30 Dec 2025 01:39:24 +1100 Subject: [PATCH 8/9] refine comment and more tc --- c++/src/capnp/arena.c++ | 4 +- c++/src/capnp/arena.h | 2 +- c++/src/capnp/layout.c++ | 18 +++-- c++/src/capnp/message-test.c++ | 143 ++++++++++++++++++++++++++++++++- c++/src/capnp/message.c++ | 5 +- c++/src/capnp/message.h | 84 +++++++++++++++++-- 6 files changed, 237 insertions(+), 19 deletions(-) diff --git a/c++/src/capnp/arena.c++ b/c++/src/capnp/arena.c++ index d64bba49a2..f0bc58e2f6 100644 --- a/c++/src/capnp/arena.c++ +++ b/c++/src/capnp/arena.c++ @@ -166,7 +166,7 @@ BuilderArena::BuilderArena(MessageBuilder* message, : message(message), segment0(this, SegmentId(0), segments[0].space.begin(), verifySegment(segments[0].space), - &this->dummyLimiter, verifySegmentSize(segments[0].wordsUsed), !segments[0].isZeroed), + &this->dummyLimiter, verifySegmentSize(segments[0].wordsUsed), segments[0].needLazyZero), needLazyZero(message->needLazyZero()){ if (segments.size() > 1) { kj::Vector> builders(segments.size() - 1); @@ -175,7 +175,7 @@ BuilderArena::BuilderArena(MessageBuilder* message, for (auto& segment: segments.slice(1, segments.size())) { builders.add(kj::heap( this, SegmentId(i++), segment.space.begin(), verifySegment(segment.space), - &this->dummyLimiter, verifySegmentSize(segment.wordsUsed), !segment.isZeroed)); + &this->dummyLimiter, verifySegmentSize(segment.wordsUsed), segment.needLazyZero)); } kj::Vector> forOutput; diff --git a/c++/src/capnp/arena.h b/c++/src/capnp/arena.h index 84e563d881..145fa1688f 100644 --- a/c++/src/capnp/arena.h +++ b/c++/src/capnp/arena.h @@ -473,7 +473,7 @@ inline SegmentBuilder::SegmentBuilder( inline SegmentBuilder::SegmentBuilder(BuilderArena* arena, SegmentId id, decltype(nullptr), ReadLimiter* readLimiter) : SegmentReader(arena, id, nullptr, ZERO * WORDS, readLimiter), - pos(nullptr), readOnly(false) {} + pos(nullptr), readOnly(false), needLazyZero_(false) {} inline word* SegmentBuilder::allocate(SegmentWordCount amount) { if (intervalLength(pos, ptr.end(), MAX_SEGMENT_WORDS) < amount) { diff --git a/c++/src/capnp/layout.c++ b/c++/src/capnp/layout.c++ index b347cee85e..cb4378d547 100644 --- a/c++/src/capnp/layout.c++ +++ b/c++/src/capnp/layout.c++ @@ -458,7 +458,7 @@ struct WireHelpers { static KJ_ALWAYS_INLINE(word* allocate( WirePointer*& ref, SegmentBuilder*& segment, CapTableBuilder* capTable, - SegmentWordCount amount, WirePointer::Kind kind, BuilderArena* orphanArena, bool zeroMemory = true)) { + SegmentWordCount amount, WirePointer::Kind kind, BuilderArena* orphanArena, bool requireCleanMemory = true)) { // Allocate space in the message for a new object, creating far pointers if necessary. The // space is guaranteed to be zero'd (because MessageBuilder implementations are required to // return zero'd memory). @@ -515,8 +515,9 @@ struct WireHelpers { ref = reinterpret_cast(ptr); ref->setKindAndTarget(kind, ptr + POINTER_SIZE_IN_WORDS, segment); - // If clean memory is wanted and the segment is not pre-zeroed, zero the data portion after the landing pad. - if (zeroMemory && segment->needLazyZero()) { + // If clean memory is wanted and the segment is not pre-zeroed, zero the data portion + // after the landing pad. + if (requireCleanMemory && segment->needLazyZero()) { WireHelpers::zeroMemory(ptr + POINTER_SIZE_IN_WORDS, amount); } @@ -525,8 +526,9 @@ struct WireHelpers { } else { ref->setKindAndTarget(kind, ptr, segment); - // If clean memory is wanted and the segment is not pre-zeroed, zero the data portion after the landing pad. - if (zeroMemory && segment->needLazyZero()) { + // If clean memory is wanted and the segment is not pre-zeroed, zero the data portion + // after the landing pad. + if (requireCleanMemory && segment->needLazyZero()) { WireHelpers::zeroMemory(ptr, amount); } return ptr; @@ -538,7 +540,7 @@ struct WireHelpers { segment = allocation.segment; ref->setKindForOrphan(kind); // Check for the need to lazy zero the segment allocated by OrphanArena. - if (zeroMemory && segment->needLazyZero()) { + if (requireCleanMemory && segment->needLazyZero()) { WireHelpers::zeroMemory(allocation.words, amount); } return allocation.words; @@ -1734,14 +1736,14 @@ struct WireHelpers { static KJ_ALWAYS_INLINE(SegmentAnd uninitializedDataPointer( WirePointer* ref, SegmentBuilder* segment, CapTableBuilder* capTable, BlobSize size, BuilderArena* orphanArena = nullptr)) { - // Allocate the space with zeroMemory = false + // Allocate the space with requireCleanMemory = false word* ptr = allocate(ref, segment, capTable, roundBytesUpToWords(size), WirePointer::LIST, orphanArena, false); // Initialize the pointer. ref->listRef.set(ElementSize::BYTE, size * (ONE * ELEMENTS / BYTES)); - // Security: Zero-out padding bytes if memory is dirty. + // Zero-out padding bytes if memory is dirty for security reason. if (segment->needLazyZero()) { size_t byteSize = unbound(size / BYTES); // Calculate the actual allocated size in bytes (aligned to Word boundary). diff --git a/c++/src/capnp/message-test.c++ b/c++/src/capnp/message-test.c++ index dfc0892e0a..247f40a76d 100644 --- a/c++/src/capnp/message-test.c++ +++ b/c++/src/capnp/message-test.c++ @@ -384,7 +384,7 @@ KJ_TEST("CustomMessageBuilder: Lazy zeroing works with custom dirty allocator") auto data = root.uninitializedDataField(3); const byte* ptr = data.begin(); - // Check Body: Should remain dirty (0xEE) because we requested "Uninitialized". + // Check Body: Should remain dirty (0xEE) because requested "Uninitialized". // This confirms the performance optimization (skipping memset on the body). KJ_EXPECT(ptr[0] == 0xEE); KJ_EXPECT(ptr[1] == 0xEE); @@ -399,6 +399,147 @@ KJ_TEST("CustomMessageBuilder: Lazy zeroing works with custom dirty allocator") KJ_EXPECT(ptr[7] == 0x00); } +KJ_TEST("STRESS: List of Structs containing Uninitialized Data") { + // Create a List of Structs. + // Each Struct contains a Data blob. + // Memory Layout: [Struct Ptr][Struct Ptr] ... [Struct Body][Data Body][Struct Body][Data Body]... + + DirtyMallocMessageBuilder builder; + auto root = builder.initRoot(); + + const int listSize = 100; + auto list = root.initStructList(listSize); + + for (int i = 0; i < listSize; ++i) { + auto element = list[i]; + + // 1. Set a primitive field (ensure clean write) + element.setInt64Field(i); + + // 2. Allocate Uninitialized Data inside the struct + // This forces the allocator to jump between Struct logic (Clean) and Data logic (Dirty/Skip) + auto data = element.uninitializedDataField(3); // 3 bytes data, 5 bytes padding + const byte* ptr = data.begin(); + + // Verify Data Body is DIRTY (0xEE) + KJ_EXPECT(ptr[0] == 0xEE); + + // Verify Data Padding is CLEAN (0x00) + KJ_EXPECT(ptr[3] == 0x00); + } + + // 3. Verify the Structs themselves remained valid (pointers didn't get corrupted) + for (int i = 0; i < listSize; ++i) { + KJ_EXPECT(list[i].getInt64Field() == i); + KJ_EXPECT(list[i].getDataField().size() == 3); + } +} + +KJ_TEST("COMPLEX: Uninitialized Data triggering Segment Expansion") { + DirtyMallocMessageBuilder builder; + auto root = builder.initRoot(); + + // Fill up the first segment (assuming default size) + auto filler = root.initDataField(8192); // 8KB + memset(filler.begin(), 0x11, filler.size()); + + // Now allocate a massive uninitialized blob that definitely forces a new Segment + // Size: 1MB + 1 byte (odd alignment) + size_t hugeSize = 1024 * 1024 + 1; + auto hugeData = root.uninitializedDataField(hugeSize); + + const byte* ptr = hugeData.begin(); + + // Verify Body (Dirty) + KJ_EXPECT(ptr[0] == 0xEE); + KJ_EXPECT(ptr[hugeSize - 1] == 0xEE); + + // Verify Padding (Clean) + KJ_EXPECT(ptr[hugeSize] == 0x00); + KJ_EXPECT(ptr[hugeSize + 6] == 0x00); +} + +KJ_TEST("CORNER CASE: Many 1-byte uninitialized allocations") { + // Allocate data where Padding (7 bytes) > Payload (1 byte). + // This stresses the zeroing logic more than the skipping logic. + + DirtyMallocMessageBuilder builder; + auto root = builder.initRoot(); + + auto list = root.initStructList(50); + + for (auto element : list) { + auto data = element.uninitializedDataField(1); + const byte* ptr = data.begin(); + + // Payload (1 byte) -> Dirty + KJ_EXPECT(ptr[0] == 0xEE); + + // Padding (7 bytes) -> Clean + for(int k=1; k<=7; ++k) KJ_EXPECT(ptr[k] == 0x00); + } +} + +KJ_TEST("CORNER CASE: Data ends exactly at word boundary minus 1 byte") { + // Case A: Size = 7 bytes. Padding = 1 byte. + // Case B: Size = 8 bytes. Padding = 0 bytes. + // Case C: Size = 9 bytes. Padding = 7 bytes. + + DirtyMallocMessageBuilder builder; + auto root = builder.initRoot(); + + // Case A: 7 bytes + { + auto data = root.uninitializedDataField(7); + const byte* ptr = data.begin(); + for(int i=0; i<7; ++i) KJ_EXPECT(ptr[i] == 0xEE); // Dirty + KJ_EXPECT(ptr[7] == 0x00); // 1 byte padding + } + + // Case B: 8 bytes (Exact fit) + { + auto data = root.uninitializedDataField(8); + const byte* ptr = data.begin(); + for(int i=0; i<8; ++i) KJ_EXPECT(ptr[i] == 0xEE); + } + + // Case C: 9 bytes (1 word + 1 byte) + { + auto data = root.uninitializedDataField(9); + const byte* ptr = data.begin(); + for(int i=0; i<9; ++i) KJ_EXPECT(ptr[i] == 0xEE); + // Padding bytes 9..15 must be zero + for(int i=9; i<16; ++i) KJ_EXPECT(ptr[i] == 0x00); + } +} + +KJ_TEST("COMPLEX: Deeply nested structs with uninitialized leaf") { + // Ensure the needLazyZero flag implies safety even deep in the graph. + // Root -> Struct A -> Struct B -> Uninitialized Data + + DirtyMallocMessageBuilder builder; + auto root = builder.initRoot(); + + // TestAllTypes has a 'structField' which is itself a TestAllTypes, allowing recursion. + auto level1 = root.initStructField(); + auto level2 = level1.initStructField(); + auto level3 = level2.initStructField(); + + auto data = level3.uninitializedDataField(10); + const byte* ptr = data.begin(); + + // Verify Dirty + KJ_EXPECT(ptr[0] == 0xEE); + KJ_EXPECT(ptr[9] == 0xEE); + + // Verify Padding Clean + KJ_EXPECT(ptr[10] == 0x00); + + // Verify Level 1 and 2 are still valid (Clean pointers) + KJ_EXPECT(level1.getBoolField() == false); + KJ_EXPECT(level2.getBoolField() == false); +} + // TODO(test): More tests. } // namespace diff --git a/c++/src/capnp/message.c++ b/c++/src/capnp/message.c++ index 1020e5f001..112c818f00 100644 --- a/c++/src/capnp/message.c++ +++ b/c++/src/capnp/message.c++ @@ -269,11 +269,14 @@ kj::ArrayPtr MallocMessageBuilder::allocateSegment(uint minimumSize) { // Allocate memory based on strategy void* result; + const char* syscallName = "unknown"; switch (initializationStrategy) { case InitializationStrategy::PRE_ZERO_MEMORY: + syscallName = "calloc(size, sizeof(word))"; result = calloc(size, sizeof(word)); break; case InitializationStrategy::LAZY_ZERO_MEMORY: + syscallName = "malloc(size * sizeof(word))"; result = malloc(size * sizeof(word)); break; default: @@ -281,7 +284,7 @@ kj::ArrayPtr MallocMessageBuilder::allocateSegment(uint minimumSize) { } if (result == nullptr) { - KJ_FAIL_SYSCALL("memory allocation failed", ENOMEM, size); + KJ_FAIL_SYSCALL(syscallName, ENOMEM, size); } if (!returnedFirstSegment) { diff --git a/c++/src/capnp/message.h b/c++/src/capnp/message.h index c1806fedad..4adc8c998a 100644 --- a/c++/src/capnp/message.h +++ b/c++/src/capnp/message.h @@ -169,7 +169,16 @@ class MessageBuilder { // Number of words in `space` which are used; the rest are free space in which additional // objects may be allocated. - bool isZeroed = true; + bool needLazyZero = false; + // Specifies whether the provided `space` contains uninitialized ("dirty") memory. + // + // If set to true, the builder assumes the segment is NOT zero-initialized. It will + // apply the same lazy zeroing logic as described in `MessageBuilder::needLazyZero()`: + // zeroing lazily as objects are allocated, while allowing specific fields (allocated + // via `initUninitializedFoo()`) to skip zeroing entirely. + // + // If false (default), Cap'n Proto assumes the segment is already zeroed, enabling + // the standard, faster allocation path. }; explicit MessageBuilder(kj::ArrayPtr segments); @@ -205,6 +214,53 @@ class MessageBuilder { // many allocators are able to provide already-zero'd memory more efficiently. virtual bool needLazyZero() const { return false; } + // Override this method in a subclass to opt in to lazy zero segment allocation. + // + // By default, Cap'n Proto requires segment allocators (including custom `allocateSegment` + // methods) to return zero-initialized memory. When the caller enables lazy zero segment + // allocation by overriding `needLazyZero` to return true, the allocator is permitted to + // return un-zeroed segments. + // + // In this case, the builder takes responsibility for zeroing during message construction: + // it will lazily zero the parts of the segment that are initialized using `initRoot()` or + // standard `initFoo()` methods, but will SKIP the regions that are explicitly initialized + // using `uninitializedFoo()` to leave them un-zeroed. This approach reduces unnecessary + // memset work for large allocations (e.g., big `DATA` type blobs) and avoids double-writing + // (memset followed by memcpy). + // + // IMPORTANT: + // 1. When opted in, the allocator does NOT need to zero the segment; Cap'n Proto handles + // zeroing as needed. + // 2. Only fields that are initialized using `uninitializedFoo()` will skip zeroing. + // All other memory will be zeroed by the builder automatically. + // 3. Currently, `uninitializedFoo()` is only supported for DATA type fields. + // 4. Users must ensure correctness: if a lazily-skipped region is read before being fully + // overwritten, the read will return uninitialized memory. Do not read skipped regions + // until you have completely written them. + // 5. Security: Before sending a message over the network, it is strongly recommended to + // explicitly zero out any dirty fields (or ensure they are fully overwritten) to + // prevent accidental leakage of previous heap data. + // + // Example usage: + // + // class LazyZeroMallocMessageBuilder : public MessageBuilder { + // public: + // // Opt-in to lazy zero segment allocation. + // bool needLazyZero() const override { return true; } + // + // kj::ArrayPtr allocateSegment(uint minimumSize) override { + // // Use malloc (dirty) instead of calloc (clean) for performance. + // void* ptr = malloc(minimumSize * sizeof(word)); + // KJ_ASSERT(ptr != nullptr); + // return kj::arrayPtr(reinterpret_cast(ptr), minimumSize); + // } + // }; + // + // LazyZeroMallocMessageBuilder builder; + // auto root = builder.initRoot(); + // // This allocation skips memset, improving performance for large blobs. + // auto data = root.uninitializedFoo(1024 * 1024); + // memcpy(data.begin(), source, data.size()); template typename RootType::Builder initRoot(); @@ -377,14 +433,26 @@ constexpr uint SUGGESTED_FIRST_SEGMENT_WORDS = 1024; constexpr AllocationStrategy SUGGESTED_ALLOCATION_STRATEGY = AllocationStrategy::GROW_HEURISTICALLY; class MallocMessageBuilder: public MessageBuilder { - // A simple MessageBuilder that uses malloc() (actually, calloc()) to allocate segments. This + // A simple MessageBuilder that uses malloc()/calloc() (by default, calloc()) to allocate segments. This // implementation should be reasonable for any case that doesn't require writing the message to // a specific location in memory. public: enum class InitializationStrategy: uint8_t { PRE_ZERO_MEMORY, + // The standard behavior: Memory for new segments is zero-initialized by the allocator + // using `calloc` before it is returned to the builder. + // + // This is the default and the safest strategy that ensures all fields default to zero. + LAZY_ZERO_MEMORY + // Memory for new segments is allocated as "dirty" / uninitialized using `malloc`. + // + // This will opt in lazy zero segment allocation, please find detailed information above + // in the MessageBuilder abstract class. + // + // BENEFIT: This enables the use of `initUninitializedData()` to skip zeroing for specific + // fields, avoiding the "double-write" (zeroing then writing/copying) for large payloads. }; explicit MallocMessageBuilder(uint firstSegmentWords = SUGGESTED_FIRST_SEGMENT_WORDS, @@ -401,16 +469,20 @@ class MallocMessageBuilder: public MessageBuilder { // zeroing it out becomes a bottleneck. // The defaults have been chosen to be reasonable for most people, so don't change them unless you // have reason to believe you need to. + // + // `initStrategy` configures how memory is initialized, please find detailed information above. explicit MallocMessageBuilder(kj::ArrayPtr firstSegment, - AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY, - InitializationStrategy initStrategy = InitializationStrategy::PRE_ZERO_MEMORY); + AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY, + InitializationStrategy initStrategy = InitializationStrategy::PRE_ZERO_MEMORY); + // This version always returns the given array for the first segment, and then proceeds with the // allocation strategy. This is useful for optimization when building lots of small messages in // a tight loop: you can reuse the space for the first segment. // - // firstSegment MUST be zero-initialized. MallocMessageBuilder's destructor will write new zeros - // over any space that was used so that it can be reused. + // By default, if lazy zero is not opted in, firstSegment MUST be zero-initialized. + // MallocMessageBuilder's destructor will write new zeros over any space that was used so that + // it can be reused. bool needLazyZero() const override { return initializationStrategy == InitializationStrategy::LAZY_ZERO_MEMORY; From 9dbefda8a9aef949559de9dbe26dfecb46e5f674 Mon Sep 17 00:00:00 2001 From: Brian Xu Date: Tue, 30 Dec 2025 13:59:45 +1100 Subject: [PATCH 9/9] fix comment --- c++/src/capnp/message.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/c++/src/capnp/message.h b/c++/src/capnp/message.h index 4adc8c998a..d169a72c29 100644 --- a/c++/src/capnp/message.h +++ b/c++/src/capnp/message.h @@ -175,7 +175,7 @@ class MessageBuilder { // If set to true, the builder assumes the segment is NOT zero-initialized. It will // apply the same lazy zeroing logic as described in `MessageBuilder::needLazyZero()`: // zeroing lazily as objects are allocated, while allowing specific fields (allocated - // via `initUninitializedFoo()`) to skip zeroing entirely. + // via `uninitializedFoo()`) to skip zeroing entirely. // // If false (default), Cap'n Proto assumes the segment is already zeroed, enabling // the standard, faster allocation path. @@ -451,7 +451,7 @@ class MallocMessageBuilder: public MessageBuilder { // This will opt in lazy zero segment allocation, please find detailed information above // in the MessageBuilder abstract class. // - // BENEFIT: This enables the use of `initUninitializedData()` to skip zeroing for specific + // BENEFIT: This enables the use of `uninitializedData()` to skip zeroing for specific // fields, avoiding the "double-write" (zeroing then writing/copying) for large payloads. };