Skip to content

Use-after-free in BrotliEncoderCompressStream when prepared dictionary retains pointer to caller buffer #1331

@hgarrereyn

Description

@hgarrereyn

There is a potential UAF when BrotliEncoderPrepareDictionary retains a stale pointer to the original dictionary buffer passed in. Other APIs have explicitly documented lifetime rules about ownership, but I couldn't find any comments/documentation about BrotliEncoderPrepareDictionary indicating the original buffer is expected to be kept alive.

Tested on the most recent commit: 85d46ce6

As a user I would expect either:

  1. BrotliEncoderPrepareDictionary makes a copy of the underlying buffer, and does not retain a pointer to the original source (thus users can free it)
  2. The lifetime requirement is explicitly documented somewhere

The following testcase triggers an ASAN violation caused by retaining a pointer to an underlying dictionary buffer which is freed before trying to perform compression:

testcase

#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
extern "C" {
#include "brotli/encode.h"
}
int main(){
  // Serialized dictionary bytes (first 115 bytes from the original reproducer)
  const uint8_t dict_bytes[115] = {
    66,82,83,68,1,0,5,0,8,0,10,0,97,81,77,85,45,55,67,121,82,112,0,0,0,0,3,0,76,54,100,0,0,27,0,178,214,247,225,95,133,228,247,4,46,184,80,61,96,134,160,126,246,1,129,131,143,116,253,138,204,172,30,0,63,164,158,165,46,220,53,169,213,146,118,50,17,174,123,243,91,168,231,143,185,77,35,160,46,192,92,234,16,90,14,0,99,76,71,80,100,113,51,121,52,76,72,57,69,51,0,199,221,182,211
  };
  BrotliEncoderPreparedDictionary* dict = nullptr;
  {
    // tmp's storage is freed at end of this scope
    std::string tmp(reinterpret_cast<const char*>(dict_bytes), sizeof(dict_bytes));
    dict = BrotliEncoderPrepareDictionary((BrotliSharedDictionaryType)0,
                                          tmp.size(),
                                          reinterpret_cast<const uint8_t*>(tmp.data()),
                                          0,
                                          nullptr,
                                          nullptr,
                                          nullptr);
    if (!dict) return 0; // bail if dictionary parse fails
  } // tmp destroyed here; if library retained pointer, UAF will occur later

  BrotliEncoderState* st = BrotliEncoderCreateInstance(nullptr,nullptr,nullptr);
  if (!st) return 0;
  (void)BrotliEncoderAttachPreparedDictionary(st, dict);

  // Drive encoder with enough data and FINISH to exercise HQ Zopfli path
  std::vector<uint8_t> in(2303);
  for (size_t i=0;i<in.size();++i) in[i] = static_cast<uint8_t>((i*131u+7u)&0xFF);
  const uint8_t* next_in = in.data();
  size_t avail_in = in.size();
  uint8_t out_buf[64];
  uint8_t* next_out = out_buf;
  size_t avail_out = sizeof(out_buf);
  size_t total_out = 0;
  while (true) {
    BROTLI_BOOL ok = BrotliEncoderCompressStream(st, BROTLI_OPERATION_FINISH,
                                                 &avail_in, &next_in,
                                                 &avail_out, &next_out,
                                                 &total_out);
    if (!ok) break;
    if (!BrotliEncoderHasMoreOutput(st) && avail_in == 0) break;
    if (avail_out == 0) { next_out = out_buf; avail_out = sizeof(out_buf); }
  }
  return 0;
}

report

{
  "Date": "2025-09-21T16:23:03.968754+00:00",
  "Uname": "Linux 6d89288ba001 5.15.0-140-generic #150-Ubuntu SMP Sat Apr 12 06:00:09 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux",
  "OS": "Ubuntu",
  "OSRelease": "22.04",
  "Architecture": "amd64",
  "ExecutablePath": "/tmp/tmp87su4xhj/reproducer",
  "ProcEnviron": [
    "LIBAFL_EDGES_MAP_SIZE=800000",
    "PWD=/fuzz/workspace",
    "CXX=gf_libafl_cxx",
    "GRAPHFUZZ_USE_ASAN=1",
    "HOME=/root",
    "ASAN_OPTIONS=hard_rss_limit_mb=1024:detect_leaks=0",
    "TERM=xterm-256color",
    "SHLVL=1",
    "LD_LIBRARY_PATH=/fuzz/install/lib",
    "PATH=/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "CC=gf_libafl_cc",
    "DEBIAN_FRONTEND=noninteractive",
    "OLDPWD=/build",
    "_=/usr/local/bin/agfi"
  ],
  "ProcCmdline": "/tmp/tmp87su4xhj/reproducer",
  "Stdin": "",
  "ProcStatus": [],
  "ProcMaps": [],
  "ProcFiles": [],
  "NetworkConnections": [],
  "CrashSeverity": {
    "Type": "NOT_EXPLOITABLE",
    "ShortDescription": "heap-use-after-free(read)",
    "Description": "Use of deallocated memory",
    "Explanation": "The target crashed when reading from memory after it has been freed."
  },
  "Stacktrace": [
    "    #0 0x55555574e379 in UpdateNodes /fuzz/src/c/enc/backward_references_hq.c:502:29",
    "    #1 0x5555557571e3 in ZopfliIterate /fuzz/src/c/enc/backward_references_hq.c:664:19",
    "    #2 0x5555557571e3 in BrotliCreateHqZopfliBackwardReferences /fuzz/src/c/enc/backward_references_hq.c:924:22",
    "    #3 0x555555671077 in EncodeData /fuzz/src/c/enc/encode.c:1104:5",
    "    #4 0x555555663847 in BrotliEncoderCompressStream /fuzz/src/c/enc/encode.c:1685:18",
    "    #5 0x55555565ec16 in main /tmp/tmp87su4xhj/reproducer.cpp:41:22",
    "    #6 0x7ffff7a6dd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16",
    "    #7 0x7ffff7a6de3f in __libc_start_main csu/../csu/libc-start.c:392:3",
    "    #8 0x5555555834c4 in _start (/tmp/tmp87su4xhj/reproducer+0x2f4c4) (BuildId: e65d5dd2e799f4fc3d57d2a874ca39d45f905fac)"
  ],
  "Registers": {},
  "Disassembly": [],
  "Package": "",
  "PackageVersion": "",
  "PackageArchitecture": "",
  "PackageDescription": "",
  "AsanReport": [
    "==156==ERROR: AddressSanitizer: heap-use-after-free on address 0x50c0000000b0 at pc 0x55555574e37a bp 0x7fffffffdec0 sp 0x7fffffffdeb8",
    "READ of size 1 at 0x50c0000000b0 thread T0",
    "    #0 0x55555574e379 in UpdateNodes /fuzz/src/c/enc/backward_references_hq.c:502:29",
    "    #1 0x5555557571e3 in ZopfliIterate /fuzz/src/c/enc/backward_references_hq.c:664:19",
    "    #2 0x5555557571e3 in BrotliCreateHqZopfliBackwardReferences /fuzz/src/c/enc/backward_references_hq.c:924:22",
    "    #3 0x555555671077 in EncodeData /fuzz/src/c/enc/encode.c:1104:5",
    "    #4 0x555555663847 in BrotliEncoderCompressStream /fuzz/src/c/enc/encode.c:1685:18",
    "    #5 0x55555565ec16 in main /tmp/tmp87su4xhj/reproducer.cpp:41:22",
    "    #6 0x7ffff7a6dd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16",
    "    #7 0x7ffff7a6de3f in __libc_start_main csu/../csu/libc-start.c:392:3",
    "    #8 0x5555555834c4 in _start (/tmp/tmp87su4xhj/reproducer+0x2f4c4) (BuildId: e65d5dd2e799f4fc3d57d2a874ca39d45f905fac)",
    "",
    "0x50c0000000b0 is located 112 bytes inside of 116-byte region [0x50c000000040,0x50c0000000b4)",
    "freed by thread T0 here:",
    "    #0 0x55555565cb3d in operator delete(void*) (/tmp/tmp87su4xhj/reproducer+0x108b3d) (BuildId: e65d5dd2e799f4fc3d57d2a874ca39d45f905fac)",
    "    #1 0x55555565e82d in __gnu_cxx::new_allocator<char>::deallocate(char*, unsigned long) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/new_allocator.h:145:2",
    "    #2 0x55555565e82d in std::allocator_traits<std::allocator<char>>::deallocate(std::allocator<char>&, char*, unsigned long) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/alloc_traits.h:496:13",
    "    #3 0x55555565e82d in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_destroy(unsigned long) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.h:245:9",
    "    #4 0x55555565e82d in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_dispose() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.h:240:4",
    "    #5 0x55555565e82d in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.h:672:9",
    "    #6 0x55555565e82d in main /tmp/tmp87su4xhj/reproducer.cpp:25:3",
    "    #7 0x7ffff7a6dd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16",
    "",
    "previously allocated by thread T0 here:",
    "    #0 0x55555565c2dd in operator new(unsigned long) (/tmp/tmp87su4xhj/reproducer+0x1082dd) (BuildId: e65d5dd2e799f4fc3d57d2a874ca39d45f905fac)",
    "    #1 0x55555565e723 in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_construct<char const*>(char const*, char const*, std::forward_iterator_tag) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.tcc:219:14",
    "    #2 0x55555565e723 in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_construct_aux<char const*>(char const*, char const*, std::__false_type) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.h:255:11",
    "    #3 0x55555565e723 in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_construct<char const*>(char const*, char const*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.h:274:4",
    "    #4 0x55555565e723 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string(char const*, unsigned long, std::allocator<char> const&) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.h:521:9",
    "    #5 0x55555565e723 in main /tmp/tmp87su4xhj/reproducer.cpp:16:17",
    "    #6 0x7ffff7a6dd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16",
    "",
    "SUMMARY: AddressSanitizer: heap-use-after-free /fuzz/src/c/enc/backward_references_hq.c:502:29 in UpdateNodes",
    "Shadow bytes around the buggy address:",
    "  0x50bffffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
    "  0x50bffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
    "  0x50bfffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
    "  0x50bfffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
    "  0x50c000000000: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd",
    "=>0x50c000000080: fd fd fd fd fd fd[fd]fa fa fa fa fa fa fa fa fa",
    "  0x50c000000100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa",
    "  0x50c000000180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa",
    "  0x50c000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa",
    "  0x50c000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa",
    "  0x50c000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa",
    "Shadow byte legend (one shadow byte represents 8 application bytes):",
    "  Addressable:           00",
    "  Partially addressable: 01 02 03 04 05 06 07",
    "  Heap left redzone:       fa",
    "  Freed heap region:       fd",
    "  Stack left redzone:      f1",
    "  Stack mid redzone:       f2",
    "  Stack right redzone:     f3",
    "  Stack after return:      f5",
    "  Stack use after scope:   f8",
    "  Global redzone:          f9",
    "  Global init order:       f6",
    "  Poisoned by user:        f7",
    "  Container overflow:      fc",
    "  Array cookie:            ac",
    "  Intra object redzone:    bb",
    "  ASan internal:           fe",
    "  Left alloca redzone:     ca",
    "  Right alloca redzone:    cb",
    "==156==ABORTING"
  ],
  "MsanReport": [],
  "UbsanReport": [],
  "LuaReport": [],
  "PythonReport": [],
  "GoReport": [],
  "JavaReport": [],
  "RustReport": [],
  "JsReport": [],
  "CSharpReport": [],
  "CrashLine": "/fuzz/src/c/enc/backward_references_hq.c:502:29",
  "Source": [
    "    498            offset = offset - addon->chunk_offsets[d] - backward;",
    "    499            limit = addon->chunk_offsets[d + 1] - addon->chunk_offsets[d] - offset;",
    "    500            limit = limit > max_len ? max_len : limit;",
    "    501            if (best_len >= limit ||",
    "--->502                continuation != source[offset + best_len]) {",
    "    503              continue;",
    "    504            }",
    "    505            len = FindMatchLengthWithLimit(&source[offset],",
    "    506                                           &ringbuffer[cur_ix_masked],",
    "    507                                           limit);"
  ]
}

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