Skip to content

node:zlib: account Zstd native allocations against external memory#6210

Open
dknecht wants to merge 1 commit intomainfrom
codex/fix-untracked-native-memory-allocation-in-zstd
Open

node:zlib: account Zstd native allocations against external memory#6210
dknecht wants to merge 1 commit intomainfrom
codex/fix-untracked-native-memory-allocation-in-zstd

Conversation

@dknecht
Copy link
Member

@dknecht dknecht commented Feb 27, 2026

Motivation

  • Zstd encoder/decoder contexts were being created with the library defaults, causing native allocations that were not tracked by the isolate external-memory accounting and enabling untrusted Workers to exhaust process memory.

Description

  • Add allocator plumbing to ZstdContext via void setAllocator(CompressionAllocator*) and a CompressionAllocator* allocator_ field so Zstd code can use the existing external-memory accounting.
  • Create Zstd contexts with ZSTD_createCCtx_advanced() / ZSTD_createDCtx_advanced() using a ZSTD_customMem that forwards to CompressionAllocator::AllocForBrotli / CompressionAllocator::FreeForZlib so native allocations are tracked.
  • Wire the allocator into both streaming and synchronous Zstd entry points by calling context()->setAllocator(&this->allocator) in ZstdCompressionStream::initialize() and creating a temporary CompressionAllocator for zstdSync() and calling ctx.setAllocator(&allocator) before initialization.
  • Changes affect src/workerd/api/node/zlib-util.h and src/workerd/api/node/zlib-util.c++ and preserve the original lifetime/behavior of Zstd contexts while enabling external-memory accounting.

Testing

  • Attempted to run bazel test //src/workerd/api/node/tests:zlib-zstd-nodejs-test@, but the test run failed in this environment because Bazel could not be downloaded from releases.bazel.build (HTTP 403), so no build/test artifacts were produced.
  • No other automated tests were executed in this environment; the diff was inspected and local compile/run was not possible here due to environment/network restrictions.

Codex Task

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm Bonk, and I've done a quick review of your PR.

This PR wires Zstd encoder/decoder contexts through CompressionAllocator so native allocations are tracked against isolate external memory, preventing untrusted Workers from silently exhausting process memory.

Issues

  1. KJ_DASSERT is debug-only — null allocator_ will crash in release (Medium severity)

    KJ_DASSERT compiles to nothing in release builds. If allocator_ is null in production (programming error / missed wiring), the code proceeds to build a ZSTD_customMem with .opaque = nullptr. The first allocation callback (AllocForBrotli) then does static_cast<CompressionAllocator*>(opaque) and dereferences it — undefined behavior / segfault.

    This is a class invariant that should be enforced in all builds. Use KJ_ASSERT (or KJ_REQUIRE) instead. See both locations below.

: ZstdContext(_mode) {}

kj::Maybe<CompressionError> ZstdEncoderContext::initialize(uint64_t pledgedSrcSize) {
KJ_DASSERT(allocator_ != nullptr, "Zstd encoder allocator should not be null");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KJ_DASSERT is stripped in release builds. If allocator_ is ever null here (e.g. a future caller forgets setAllocator), this will silently pass through and crash inside the ZSTD allocation callback when it dereferences the null opaque pointer. Use KJ_ASSERT so the invariant is enforced in all build modes:

Suggested change
KJ_DASSERT(allocator_ != nullptr, "Zstd encoder allocator should not be null");
KJ_ASSERT(allocator_ != nullptr, "Zstd encoder allocator should not be null");

kj::Maybe<CompressionError> ZstdDecoderContext::initialize() {
// dctx_ is created in the constructor. It can only be nullptr if ZSTD_createDCtx()
// failed due to memory allocation failure.
KJ_DASSERT(allocator_ != nullptr, "Zstd decoder allocator should not be null");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the encoder path — KJ_DASSERT is a no-op in release builds:

Suggested change
KJ_DASSERT(allocator_ != nullptr, "Zstd decoder allocator should not be null");
KJ_ASSERT(allocator_ != nullptr, "Zstd decoder allocator should not be null");

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Feb 27, 2026

Review posted successfully. Here's a summary of what I found:

PR #6210 wires Zstd encoder/decoder contexts through CompressionAllocator so native allocations are tracked against V8 isolate external memory — a good security fix preventing untrusted Workers from exhausting process memory untracked.

One actionable issue found: Both KJ_DASSERT calls (encoder at line 818, decoder at line 893) should be KJ_ASSERT instead. KJ_DASSERT is stripped in release builds, so a null allocator_ would silently pass through and cause a null-pointer dereference inside the ZSTD allocation callback. I posted inline suggestions for both locations.

github run

@github-actions
Copy link

The generated output of @cloudflare/workers-types has been changed by this PR. If this is intentional, run just generate-types to update the snapshot. Alternatively, you can download the full generated types:

Full Type Diff

@jasnell
Copy link
Collaborator

jasnell commented Feb 27, 2026

Some compile errors here need to be looked at

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants