Project: nanopb (nanopb/nanopb), language: c++
Vulnerability instance: nanopb.cve-2020-26243
Sanitizer: address

================= Bug Report (1/2) ==================
## Source: GitHub Advisory
## URL: https://github.com/nanopb/nanopb/security/advisories/GHSA-85rr-4rh9-hhwh
## Description:
Advisory ID: GHSA-85rr-4rh9-hhwh
Severity: medium
Title: Memory leak in Nanopb
Published: 2020-11-25 16:53:27+00:00
Updated: 2023-01-09 05:03:56+00:00


Affected Packages:
Package: nanopb (Vulnerable: >= 0.3.2, <= 0.3.9.6)
Package: nanopb (Vulnerable: >= 0.4.0, < 0.4.4)

Description:
### Impact
Decoding specifically formed message can leak memory if dynamic allocation is enabled and an oneof field contains a static submessage that contains a dynamic field, and the message being decoded contains the submessage multiple times. This is rare in normal messages, but it is a concern when untrusted data is parsed.

### Patches
Preliminary patch is [available on git](https://github.com/nanopb/nanopb/commit/edf6dcbffee4d614ac0c2c1b258ab95185bdb6e9) and problem will be patched in versions 0.3.9.7 and 0.4.4 once testing has been completed.

### Workarounds
Following workarounds are available:
* Set the option `no_unions` for the oneof field. This will generate fields as separate instead of C union, and avoids triggering the problematic code.
* Set the type of the submessage field inside oneof to `FT_POINTER`. This way the whole submessage will be dynamically allocated and the problematic code is not executed.
* Use an arena allocator for nanopb, to make sure all memory can be released afterwards.

### References
Bug report: https://github.com/nanopb/nanopb/issues/615

### For more information
If you have any questions or comments about this advisory, comment on the bug report linked above.


References:
https://github.com/nanopb/nanopb/security/advisories/GHSA-85rr-4rh9-hhwh
https://github.com/nanopb/nanopb/issues/615
https://github.com/nanopb/nanopb/commit/4fe23595732b6f1254cfc11a9b8d6da900b55b0c
https://github.com/nanopb/nanopb/blob/2b48a361786dfb1f63d229840217a93aae064667/CHANGELOG.txt
https://nvd.nist.gov/vuln/detail/CVE-2020-26243
https://github.com/advisories/GHSA-85rr-4rh9-hhwh

================= Bug Report (2/2) ==================
## Source: GitHub Issue
## URL: https://github.com/nanopb/nanopb/issues/615
## Description:
Issue: nanopb/nanopb#615
Title: Memory leak when parsing a protobuf message with duplicate fields
State: closed
Created by: niooss-ledger
Created at: 2020-11-24 17:26:04+00:00
Labels: Type-Defect, Component-Decoder, Priority-High, FixedInGit

Issue Body:
Hello,

While fuzzing a project that relies on Nanopb to parse (untrusted) user input, I found a memory leak which is triggered by sending some message where fields are duplicated.

**Steps to reproduce the issue**

In order to test this memleak on several versions of Nanopb (and several Linux distributions), I have written the following script:
```sh
#!/bin/sh
# Reproduce a memory leak issue in nanopb parser
#
# Dependencies on Debian: sudo apt install clang git protobuf-compiler python3 python3-protobuf
set -e -x

# Clone nanopb
if ! [ -d nanopb ] ; then
    git clone https://github.com/nanopb/nanopb
fi

# Create a protobuf file for some message with a header
cat > mypackage.proto << EOF
syntax = "proto3";
package mypackage;

import "nanopb.proto";

message HeaderField {
  bytes mydata = 1 [(nanopb).type = FT_POINTER];
}

message Header {
  option (nanopb_msgopt).anonymous_oneof = true;
  oneof one {
    HeaderField field = 1;
  }
}

message MessageWithHeader {
  Header head = 1;
}
EOF

# Create a fuzzer on this message
cat > fuzz_decode_message.c << EOF
#include <stdint.h>
#include <stdio.h>

#include <pb_decode.h>
#include "mypackage.pb.h"

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    mypackage_MessageWithHeader req = {};

    pb_istream_t is = pb_istream_from_buffer(data, size);
    if (!pb_decode(&is, mypackage_MessageWithHeader_fields, &req)) {
        printf("Failed to decode input: %s\n", PB_GET_ERROR(&is));
        return 0;
    }
    printf("Parsing ok, req.head.which_one = %u\n", req.head.which_one);
    pb_release(mypackage_MessageWithHeader_fields, &req);
    return 0;
}
EOF

# Compile the .proto and the fuzzer
protoc \
    -Inanopb/generator \
    -Inanopb/generator/proto \
    -I. \
    --plugin=protoc-gen-nanopb=nanopb/generator/protoc-gen-nanopb \
    --nanopb_opt= \
    --nanopb_out=. \
    mypackage.proto

clang -g -ggdb -O1 -fsanitize=fuzzer,address,undefined \
    -Wall -Wextra -Inanopb -DPB_ENABLE_MALLOC -DPB_FIELD_32BIT \
    -o fuzz_decode_message.out \
    fuzz_decode_message.c mypackage.pb.c nanopb/pb_decode.c nanopb/pb_common.c

# Run on a test case that leaks some bytes
python3 -c 'import sys;sys.stdout.buffer.write(bytes.fromhex("0a06 0a020a00 0a00"))' > memleak_message
./fuzz_decode_message.out memleak_message
```

**What happens?**

On a up-to-date Debian 10 machine, this leads to the following output:
```text
./fuzz_decode_message.out: Running 1 inputs 1 time(s) each.
Running: memleak_message
Parsing ok, req.head.which_one = 1
Parsing ok, req.head.which_one = 1

=================================================================
==3937==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 4 byte(s) in 1 object(s) allocated from:
    #0 0x4f25a2 in realloc (/fuzz_decode_message.out+0x4f25a2)
    #1 0x536a80 in allocate_field /nanopb/pb_decode.c:581:11
    #2 0x533f3a in pb_dec_bytes /nanopb/pb_decode.c:1479:14
    #3 0x52ed88 in decode_pointer_field /nanopb/pb_decode.c
    #4 0x525632 in pb_decode_inner /nanopb/pb_decode.c:1083:14
    #5 0x5359cd in pb_dec_submessage /nanopb/pb_decode.c:1589:18
    #6 0x52d008 in decode_static_field /nanopb/pb_decode.c:532:20
    #7 0x525632 in pb_decode_inner /nanopb/pb_decode.c:1083:14
    #8 0x5359cd in pb_dec_submessage /nanopb/pb_decode.c:1589:18
    #9 0x52cea9 in decode_static_field /nanopb/pb_decode.c
    #10 0x525632 in pb_decode_inner /nanopb/pb_decode.c:1083:14
    #11 0x526c24 in pb_decode /nanopb/pb_decode.c:1159:14
    #12 0x52143d in LLVMFuzzerTestOneInput /fuzz_decode_message.c:11:10
    #13 0x42edfa in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/fuzz_decode_message.out+0x42edfa)
    #14 0x422003 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/fuzz_decode_message.out+0x422003)
    #15 0x426b31 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/fuzz_decode_message.out+0x426b31)
    #16 0x44a3f2 in main (/fuzz_decode_message.out+0x44a3f2)
    #17 0x7fd6f93e609a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a)

SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).

INFO: a leak has been found in the initial corpus.
```

With my program, `0a06 0a020a00 0a00` leaks 4 bytes, `0a0a 0a020a00 0a020a00 0a00` leaks 8 bytes, etc.

**What should happen?**

I believe that parsing untrusted input should not leak allocated memory. You might disagree with this belief, in which case it would be nice to indicate in https://github.com/nanopb/nanopb/security/policy that Nanopb may leak memory when parsing untrusted data which was maliciously crafted.


Comments:
Comment by PetteriAimonen on 2020-11-24 19:35:11+00:00:
I agree with you that parsing untrusted input shouldn't leak memory, and the fuzztest included in nanopb tries to verify that also.

It appears that your test case is hitting some situation that is not covered by the fuzztest. I'll have to take a look.

---

Comment by PetteriAimonen on 2020-11-24 20:13:28+00:00:
Looks like the current test cases did not find this because they contained only a fully static fields inside a static submessage inside oneof, and pointer fields inside a pointer submessage inside oneof. And the bug is only triggered by pointer fields inside a static submessage inside oneof.

There is a preliminary fix on git now, but test cases must still be expanded to cover this. Something like #143 would be nice to help finding cases like this, but I'm not sure how to go about actually implementing that.

---

Comment by niooss-ledger on 2020-11-25 08:11:42+00:00:
Thanks for your quick reply. I confirm your patches fix the memory leak in the project that I am fuzzing (I updated Nanopb to `master` branch, which includes https://github.com/nanopb/nanopb/commit/edf6dcbffee4d614ac0c2c1b258ab95185bdb6e9).

I am not familiar enough with the project to help adding what would be necessary to generate `.proto` files by fuzzing which would have detected this issue (like what is described in https://github.com/nanopb/nanopb/issues/143) but I will continue to fuzz some projects that use Nanopb and report other issues that I might find.

For this issue, do you know when a release will include a fix? (In a few weeks/months/...?). Also, is there any plan regarding back-porting the fix to branch `maintenance_0.3`?

---

Comment by PetteriAimonen on 2020-11-25 09:23:56+00:00:
Yeah, I'm working on making a release, you can expect it today or tomorrow. And yeah, I will backport the fix and the test to 0.3 also.

---

Comment by PetteriAimonen on 2020-11-25 13:21:48+00:00:
Fix is now released in 0.4.4 and 0.3.9.7.

Commit References:
edf6dcbffee4d614ac0c2c1b258ab95185bdb6e9

Build script:
#!/bin/bash
# Default build script template for C/C++ projects
set -eu

# Define configuration options
CONFIG_OPTS="--disable-shared"
CMAKE_OPTS="-DCMAKE_BUILD_TYPE=Debug"
MAKE_OPTS="-j$(nproc)"
BUILD_DIR="build"

# 1. Run build preparation scripts if they exist
for prep_script in buildconf autogen.sh bootstrap; do
  if [ -x "$prep_script" ]; then
    echo "Running $prep_script..."
    ./$prep_script
    break
  elif [ -f "$prep_script" ]; then
    echo "Found $prep_script but it's not executable, setting permissions..."
    chmod +x $prep_script
    ./$prep_script
    break
  fi
done

# 2. Handle different build systems
if [ -f "configure" ]; then
  echo "Found configure script, running with options: $CONFIG_OPTS"
  ./configure $CONFIG_OPTS

  echo "Building with make $MAKE_OPTS"
  make $MAKE_OPTS
elif [ -f "CMakeLists.txt" ]; then
  echo "Found CMakeLists.txt, using CMake build system"
  mkdir -p $BUILD_DIR
  cd $BUILD_DIR
  cmake $CMAKE_OPTS ..
  make $MAKE_OPTS
elif [ -f "Makefile" ] || [ -f "makefile" ]; then
  echo "Found Makefile, building with make $MAKE_OPTS"
  make $MAKE_OPTS
elif [ -f "meson.build" ]; then
  echo "Found meson.build, using Meson build system"
  mkdir -p $BUILD_DIR
  meson setup $BUILD_DIR .
  cd $BUILD_DIR
  ninja
elif [ -f "GNUmakefile" ]; then
  echo "Found GNUmakefile, building with make $MAKE_OPTS"
  make $MAKE_OPTS
elif [ -d ".git" ] && [ -f "Makefile.am" ]; then
  echo "Found Makefile.am, running autoreconf"
  autoreconf -i
  ./configure $CONFIG_OPTS
  make $MAKE_OPTS
else
  echo "WARNING: No recognized build system found!"
  echo "Trying generic approach:"
  
  # Try to detect common source patterns and compile them
  if ls *.c >/dev/null 2>&1 || ls *.cpp >/dev/null 2>&1 || ls *.cc >/dev/null 2>&1; then
    echo "Found C/C++ source files, attempting direct compilation"
    if ls *.cpp >/dev/null 2>&1 || ls *.cc >/dev/null 2>&1; then
      CXX=${CXX:-g++}
      echo "Compiling C++ sources with $CXX"
      $CXX -g -O1 -fsanitize=address -fno-omit-frame-pointer -o main *.cpp *.cc 2>/dev/null || true
    elif ls *.c >/dev/null 2>&1; then
      CC=${CC:-gcc}
      echo "Compiling C sources with $CC"
      $CC -g -O1 -fsanitize=address -fno-omit-frame-pointer -o main *.c 2>/dev/null || true
    fi
  fi
fi

Dockerfile:
FROM hwiwonlee/secb.base:latest
RUN apt-get update && apt-get install -y python3-pip git wget
RUN git clone https://github.com/nanopb/nanopb nanopb
RUN git -C nanopb checkout d9d5dfd869aca4d00a81f671b2445fb4cea0352f
WORKDIR $SRC/nanopb
COPY build.sh $SRC/

Additional files: