diff --git a/c++/src/capnp/arena.c++ b/c++/src/capnp/arena.c++ index 7e6c4ba7ad..f0bc58e2f6 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)) { + &this->dummyLimiter, verifySegmentSize(segments[0].wordsUsed), segments[0].needLazyZero), + needLazyZero(message->needLazyZero()){ if (segments.size() > 1) { kj::Vector> builders(segments.size() - 1); @@ -173,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))); + &this->dummyLimiter, verifySegmentSize(segment.wordsUsed), segment.needLazyZero)); } kj::Vector> forOutput; @@ -224,6 +226,9 @@ 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)); @@ -232,10 +237,13 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { // 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, needLazyZero); segmentWithSpace = &segment0; - return AllocateResult { &segment0, segment0.allocate(amount) }; + word* resultPtr = segment0.allocate(amount); + // Zero the root pointer field if the memory is not pre-zeroed. + if (needLazyZero) 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. @@ -252,13 +260,17 @@ BuilderArena::AllocateResult BuilderArena::allocate(SegmentWordCount amount) { } // Need to allocate a new segment. - SegmentBuilder* result = addSegmentInternal(message->allocateSegment(unbound(amount / WORDS))); + SegmentBuilder* result = addSegmentInternal(message->allocateSegment(unbound(amount / WORDS)), + needLazyZero); // 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. - return AllocateResult { result, result->allocate(amount) }; + word* resultPtr = result->allocate(amount); + // Zero the root pointer field if the memory is not pre-zeroed. + if (needLazyZero) memset(resultPtr, 0, static_cast(POINTER_SIZE_IN_WORDS) * sizeof(word)); + return AllocateResult { result, resultPtr }; } } @@ -267,7 +279,7 @@ SegmentBuilder* BuilderArena::addExternalSegment(kj::ArrayPtr conten } template -SegmentBuilder* BuilderArena::addSegmentInternal(kj::ArrayPtr content) { +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, @@ -286,7 +298,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, 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 7308912b80..145fa1688f 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 needLazyZero = false); inline SegmentBuilder(BuilderArena* arena, SegmentId id, const word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter); + ReadLimiter* readLimiter, SegmentWordCount wordsUsed = ZERO * WORDS, + bool needLazyZero = 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 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`. // Otherwise, do nothing. @@ -224,6 +228,8 @@ class SegmentBuilder: public SegmentReader { bool readOnly; + bool needLazyZero_; + [[noreturn]] void throwNotWritable(); KJ_DISALLOW_COPY_AND_MOVE(SegmentBuilder); @@ -376,7 +382,9 @@ 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 needLazyZero = false); + + bool needLazyZero = false; }; // ======================================================================================= @@ -452,20 +460,20 @@ 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 needLazyZero) : SegmentReader(arena, id, ptr, size, readLimiter), - pos(ptr + wordsUsed), readOnly(false) {} + pos(ptr + wordsUsed), readOnly(false), needLazyZero_(needLazyZero) {} inline SegmentBuilder::SegmentBuilder( BuilderArena* arena, SegmentId id, const word* ptr, SegmentWordCount size, - ReadLimiter* readLimiter) + 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) {} + 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), - 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/compiler/capnpc-c++.c++ b/c++/src/capnp/compiler/capnpc-c++.c++ index 262ccb726b..9798c061c7 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, " 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"), @@ -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::uninitialized", titleCase, "(unsigned int size) {\n", + unionDiscrim.set, + " return _builder.getPointerField(\n" + " ::capnp::bounded<", offset, ">() * ::capnp::POINTERS).uninitializedBlob<", 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..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)) { + 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,10 +515,22 @@ 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 (requireCleanMemory && segment->needLazyZero()) { + 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); + + // 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; } } else { @@ -527,6 +539,10 @@ struct WireHelpers { auto allocation = orphanArena->allocate(amount); segment = allocation.segment; ref->setKindForOrphan(kind); + // Check for the need to lazy zero the segment allocated by OrphanArena. + if (requireCleanMemory && segment->needLazyZero()) { + WireHelpers::zeroMemory(allocation.words, amount); + } return allocation.words; } } @@ -1717,6 +1733,32 @@ struct WireHelpers { return { segment, Data::Builder(reinterpret_cast(ptr), unbound(size / BYTES)) }; } + static KJ_ALWAYS_INLINE(SegmentAnd uninitializedDataPointer( + WirePointer* ref, SegmentBuilder* segment, CapTableBuilder* capTable, BlobSize size, + BuilderArena* orphanArena = nullptr)) { + // 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)); + + // 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). + 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 +2637,11 @@ Data::Builder PointerBuilder::initBlob(ByteCount size) { assertMaxBits(size, ThrowOverflow())).value; } template <> +Data::Builder PointerBuilder::uninitializedBlob(ByteCount size) { + return WireHelpers::uninitializedDataPointer(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..240b1d61b6 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 uninitializedBlob(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::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 bb5acca711..247f40a76d 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,332 @@ KJ_TEST("MessageBuilder::sizeInWords()") { KJ_EXPECT(reader.sizeInWords() == expected); } +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::LAZY_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::LAZY_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.uninitializedDataField(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 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::LAZY_ZERO_MEMORY); + + // Initialize a Struct. + // 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(); + + // 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::LAZY_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.uninitializedDataField(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::LAZY_ZERO_MEMORY); + auto root = builder.initRoot(); + + auto data = root.uninitializedDataField(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) +// 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() + // 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) { + free(ptr); + } + } + + // Explicitly declare that this allocator provides dirty memory. + // This forces the MessageBuilder to proactively zero-out Structs upon initialization. + bool needLazyZero() const override { return true; } + + kj::ArrayPtr allocateSegment(uint minimumSize) override { + size_t sizeBytes = minimumSize * sizeof(word); + void* ptr = malloc(sizeBytes); + KJ_ASSERT(ptr != nullptr); + + // 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); + 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 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 of Data. + // Memory Layout (1 word): [Body: 0, 1, 2] [Padding: 3, 4, 5, 6, 7] + auto data = root.uninitializedDataField(3); + const byte* ptr = data.begin(); + + // 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); + KJ_EXPECT(ptr[2] == 0xEE); + + // 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); + KJ_EXPECT(ptr[6] == 0x00); + 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 d08caf9d62..112c818f00 100644 --- a/c++/src/capnp/message.c++ +++ b/c++/src/capnp/message.c++ @@ -210,19 +210,21 @@ 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."); - // 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::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."); + } } MallocMessageBuilder::~MallocMessageBuilder() noexcept(false) { @@ -265,9 +267,24 @@ kj::ArrayPtr MallocMessageBuilder::allocateSegment(uint minimumSize) { uint size = kj::max(minimumSize, nextSize); - void* result = calloc(size, sizeof(word)); + // 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: + KJ_FAIL_ASSERT("Unknown initialization strategy"); + } + if (result == nullptr) { - KJ_FAIL_SYSCALL("calloc(size, sizeof(word))", 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 8ca385eadf..d169a72c29 100644 --- a/c++/src/capnp/message.h +++ b/c++/src/capnp/message.h @@ -168,6 +168,17 @@ 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 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 `uninitializedFoo()`) 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); @@ -202,6 +213,55 @@ 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 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(); // Initialize the root struct of the message as the given struct type. @@ -373,13 +433,31 @@ 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 `uninitializedData()` 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, - AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY); + AllocationStrategy allocationStrategy = SUGGESTED_ALLOCATION_STRATEGY, + 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: @@ -391,15 +469,24 @@ 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); + 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; + } KJ_DISALLOW_COPY_AND_MOVE(MallocMessageBuilder); virtual ~MallocMessageBuilder() noexcept(false); @@ -409,6 +496,7 @@ class MallocMessageBuilder: public MessageBuilder { private: uint nextSize; AllocationStrategy allocationStrategy; + InitializationStrategy initializationStrategy; bool ownFirstSegment; bool returnedFirstSegment;