Skip to content

NULL pointer dereference in exportPairs() on allocation failure (proxy_wasm_api.h) #202

Description

@jimaf

NULL Pointer Dereference in exportPairs() (proxy_wasm_api.h)

Component: proxy-wasm-cpp-sdk (guest-side SDK header, proxy_wasm_api.h)
Pinned commit examined: e5256b0c5463ea9961965ad5de3e379e00486640
Confirmed still present verbatim at current upstream main as of 2026-09-09.
CWE: CWE-476 (NULL Pointer Dereference)

Summary

exportPairs() allocates a marshalling buffer with ::malloc(size) and
passes the result straight to marshalPairs(), which writes through it
unconditionally, without ever checking whether the allocation succeeded:

template <typename Pairs> void exportPairs(const Pairs &pairs, const char **ptr, size_t *size_ptr) {
  if (pairs.empty()) {
    *ptr = nullptr;
    *size_ptr = 0;
    return;
  }
  size_t size = pairsSize(pairs);
  char *buffer = static_cast<char *>(::malloc(size));
  marshalPairs(pairs, buffer);   // <-- no NULL check before this write
  *size_ptr = size;
  *ptr = buffer;
}

marshalPairs()'s first action is an unconditional write through buffer
(a uint32_t length-prefix write), followed by further writes/memcpys
for each key/value pair. If malloc() returns NULL -- under memory
pressure, or when marshalling a sufficiently large set of headers/pairs --
this becomes a write through address 0x0.

Affected call sites

exportPairs() is a template used by two SDK convenience wrappers in the
same file, both of which inherit the same defect with no additional
mitigation:

  • sendLocalResponse(uint32_t response_code, ...) (~line 1112, free
    function -- body calling exportPairs() around ~line 1118)
  • setHeaderMapPairs(WasmHeaderMapType, const HeaderStringPairs &) (~line 1244)

Any caller of these two functions -- or of exportPairs() directly -- is
exposed. This is not specific to any one embedder; it affects any
proxy-wasm-cpp-sdk guest module consumer (Envoy's WASM filter path is one
such consumer, and is how this was originally found while auditing a
vendored copy of this header, but the defect is upstream in
proxy-wasm-cpp-sdk itself, not an Envoy-specific issue).

Root cause

Straightforward missing-NULL-check-after-allocation. Grepping the file for
other ::malloc(size) call sites shows the same pattern repeated with no
existing convention for handling allocation failure anywhere in this file:

  • getProperty(const std::initializer_list<std::string_view> &) (~line 909)
  • getProperty(const std::vector<S> &) (~line 936)
  • MakeHeaderStringPairsBuffer(...) (~line 1396)

All four sites call ::malloc() and immediately write through the result
with no check. There is no existing malloc-null-check idiom elsewhere in
the file to match, so a fix has to introduce the convention rather than
follow one.

Suggested fix

Add a NULL check on buffer immediately after the malloc() call in
exportPairs(), e.g.:

size_t size = pairsSize(pairs);
char *buffer = static_cast<char *>(::malloc(size));
if (buffer == nullptr) {
  *ptr = nullptr;
  *size_ptr = 0;
  return;
}
marshalPairs(pairs, buffer);
*size_ptr = size;
*ptr = buffer;

(Callers of exportPairs() already need to handle *ptr == nullptr as a
valid "empty" case for the zero-pairs path, so this reuses an existing
contract rather than introducing a new one.) The same missing-check pattern
at the three other ::malloc() sites listed above should be considered as
part of the same fix, since they share the identical defect shape.

Evidence: ASan crash reproduction

Built a minimal standalone harness (test_exportpairs_nullderef.cc)
against the pinned-commit header, compiled with
-fsanitize=address -g -O0 -std=c++17, calling exportPairs() directly
with one key/value pair sized so the marshalled buffer requires
~5,000,017 bytes.

Forcing the allocation to fail (ASAN_OPTIONS=allocator_may_return_null=1:max_allocation_size_mb=1)
produces:

==2048==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x61c16549dea7 ...)
==2048==The signal is caused by a WRITE memory access.
==2048==Hint: address points to the zero page.
    #0 ... in marshalPairs<...>(...) proxy_wasm_api.h:177
    #1 ... in exportPairs<...>(...) proxy_wasm_api.h:205
    #2 ... in main test_exportpairs_nullderef.cc:58

Control run, identical harness and input, no allocation cap
(ASAN_OPTIONS=allocator_may_return_null=0): completes normally, prints
exportPairs succeeded: ptr=0x783493273800 size=5000017, exits 0 --
confirming the crash above is specifically the injected allocation
failure, not a harness defect. Full build command and both runs' complete
stdout/stderr are captured in the accompanying summary file.

Impact

  • Denial of Service only. This is a write to a fixed, predictable
    address (0x0) with no attacker-controlled offset or content -- there is
    no plausible path to memory corruption or code execution from this
    specific bug.
  • Realistically triggered under general memory pressure (a guest
    module marshalling headers/pairs while the host process is already
    under allocation stress), not by a single pathological request; the
    marshalled size is nonetheless attacker-influenceable via the size and
    number of header/pair entries being exported, which affects how much
    memory pressure is needed to hit this path.
  • Affects any proxy-wasm-cpp-sdk consumer, not just Envoy -- any
    embedder linking this SDK's guest-side header inherits the same
    unchecked-allocation pattern in exportPairs(), getProperty() (x2),
    and [MakeHeaderStringPairsBuffer().](url)

proxy_wasm_exportpairs_poc_20260909_090734_SUMMARY.txt

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions