Skip to content

Fix silent data loss when log message exceeds stackalloc buffer#7

Merged
DJGosnell merged 1 commit intomasterfrom
fix/arraypool-fallback-on-buffer-overflow
Feb 20, 2026
Merged

Fix silent data loss when log message exceeds stackalloc buffer#7
DJGosnell merged 1 commit intomasterfrom
fix/arraypool-fallback-on-buffer-overflow

Conversation

@DJGosnell
Copy link
Member

Summary

Utf8LogWriter previously dropped writes silently when the fixed stackalloc buffer was too small. This caused entire string parameters (e.g. long Vulkan validation messages, stack traces, serialized payloads) to vanish from log output with no indication. The writer now falls back to ArrayPool<byte> on overflow, with Dispose() returning the rented buffer after dispatch.

Reason for the change

Users reported that runtime string values containing text like {VkBuffer 0x...} appeared to be "eaten" — when in reality the entire string parameter was being silently dropped because it exceeded the 128-byte budget allocated for string params in the stackalloc buffer. This affects any log method where the message content exceeds the estimated buffer size (clamped at 4096 bytes max), which is common for:

  • Vulkan/graphics validation layer messages
  • Full stack traces
  • Serialized objects / JSON payloads
  • SQL queries
  • Verbose diagnostic output

Impacts of changes

  • Utf8LogWriter: All write methods (Write, WriteString, WriteFormatted) now grow via ArrayPool<byte>.Shared when the stackalloc buffer is exhausted, instead of silently discarding content.
  • Grow(): Marked [MethodImpl(MethodImplOptions.NoInlining)] to keep the hot path (no overflow) lean and inlineable.
  • Generated code: Now emits writer.Dispose() after dispatch to return any rented buffer.
  • 8 new/updated unit tests covering overflow scenarios for literals, strings, and formatted values.

Migration Steps

None. This is a transparent behavioral fix. No API surface changes, no configuration required.

Performance Considerations

  • Hot path (no overflow): Zero overhead. The stackalloc buffer is used as before. Dispose() is a null check on _rented.
  • Cold path (overflow): One ArrayPool<byte>.Shared.Rent + copy of existing bytes + Return on dispose. This is the standard .NET pooling pattern and avoids GC pressure.
  • WriteString computes the exact byte count needed via Encoding.UTF8.GetByteCount() so it grows precisely once.
  • WriteFormatted retries in a doubling loop since the exact formatted size isn't known upfront.

Security Considerations

None. No new input surfaces, no external data handling changes.

Breaking changes

Public consumer-facing changes

None. Utf8LogWriter is a ref struct used internally by generated code. The public API surface is unchanged.

Internal non-consumer changes

  • Utf8LogWriter._buffer changed from readonly Span<byte> to Span<byte> (required for Grow to reassign it).
  • Utf8LogWriter gains a byte[]? _rented field and Dispose() method.
  • Generated method bodies now include a writer.Dispose() call after dispatch.

🤖 Generated with Claude Code

Utf8LogWriter previously dropped writes silently when the fixed
stackalloc buffer was too small. This caused entire string parameters
(e.g. long Vulkan validation messages, stack traces, serialized
payloads) to vanish from log output with no indication.

Now falls back to ArrayPool<byte> on overflow, with Dispose() returning
the rented buffer after dispatch. The hot path (message fits in
stackalloc) remains zero-allocation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@DJGosnell DJGosnell merged commit 88ab039 into master Feb 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant