trevdatastreams's picture
Add Apache Avro negative object-count PoC
73c257a verified
|
Raw
History Blame Contribute Delete
4.46 kB
# Apache Avro C++ negative object-count infinite record stream
## Summary
Apache Avro C++ accepts a negative top-level object count in an Object
Container File data block. `DataFileReaderBase::hasMore()` treats every nonzero
count as available data, and `DataFileReader<T>::read()` decrements the count
after every successful record.
An empty-record schema consumes zero bytes per record. A 113-byte file with
`objectCount_ == -1` therefore makes the public C++ reader return records
continuously without advancing the data stream or reaching EOF.
The control differs by one byte:
- control block count: `1`, Avro zig-zag byte `0x02`;
- trigger block count: `-1`, Avro zig-zag byte `0x01`;
- mutation offset: `95`.
## Affected revision
- Apache Avro commit:
`44f7aa35a6b90ac7d39ef0e68dad706561235941`
- Component: `lang/c++`
- Public API: `avro::DataFileReader<avro::GenericDatum>::read()`
## Root cause
`lang/c++/impl/DataFile.cc:541` stores the untrusted signed block count without
validating that it is non-negative:
```cpp
avro::decode(*decoder_, objectCount_);
```
`DataFile.cc:470-476` treats `-1` as data:
```cpp
if (eof_) {
return false;
} else if (objectCount_ != 0) {
return true;
}
```
`lang/c++/include/avro/DataFile.hh:402-406` then decrements the negative count
and decodes another record:
```cpp
if (base_->hasMore()) {
base_->decr();
avro::decode(base_->decoder(), datum);
return true;
}
```
For an empty record, `avro::decode()` consumes zero bytes. The sequence is
`-1, -2, -3, ...`; `hasMore()` never observes zero.
## Reproduction
Build current Avro C++ with sanitizers:
```bash
cmake -S /path/to/avro/lang/c++ -B /path/to/avro-build \
-DCMAKE_BUILD_TYPE=Debug \
-DAVRO_BUILD_TESTS=OFF \
-DAVRO_BUILD_EXECUTABLES=ON \
-DAVRO_BUILD_SHARED=OFF \
-DAVRO_BUILD_STATIC=ON \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
cmake --build /path/to/avro-build --parallel 4
```
Run the packaged reproducer:
```bash
AVRO_SRC=/path/to/avro/lang/c++ \
AVRO_BUILD=/path/to/avro-build \
./reproduce.sh
```
Expected output:
```text
== control ==
EOF records=1
== trigger ==
LIMIT_REACHED records=1000000 reader_still_returning=true
trigger reproduced: the reader returned 1,000,000 zero-byte records without EOF
```
The one-million-record cap belongs to the PoC harness. Removing it leaves the
ordinary `while (reader.read(datum))` loop running indefinitely.
## Verification
Three instrumented runs used a five-million-record safety cap:
- control: EOF after exactly one record in 3/3 runs;
- trigger: still returned records at 5,000,000 in 3/3 runs;
- trigger user CPU time: 0.57–0.58 seconds per capped run;
- sanitizer errors: none required; this is a parser state-machine flaw.
Artifacts:
- `models/control-one-empty-record.avro`
- size: 113 bytes
- SHA-256:
`01f0e4309fd0e1a1b222945b98050e4982d83afc7c107a58cfe2306d8ba32463`
- `models/trigger-negative-object-count.avro`
- size: 113 bytes
- SHA-256:
`8fc76abb89503e59d424a66de7e4775dfb0a7a1be4a29f3fd64d51301d87257d`
## Security impact
A service, converter, ETL process, or CLI that iterates over records from an
untrusted Avro OCF can enter an unbounded CPU loop. Downstream consumers that
materialize, count, log, or forward the returned records also experience
unbounded output amplification from a 113-byte file.
The file does not need compression, a large allocation, or a large payload.
The malformed count is the only changed byte.
## Suggested fix
Reject negative top-level data-block object counts immediately after decoding
them in `DataFileReaderBase::readDataBlock()`. Also reject negative
`byteCount` values before converting them to `size_t`.
Add regression cases for:
- `objectCount_ == -1` with an empty-record schema;
- `objectCount_ == INT64_MIN`;
- negative `byteCount`;
- zero-count and one-count valid controls.
## Prior art
The public scan found no exact report of the top-level C++ OCF `objectCount_`
state-machine flaw.
Closest distinct work:
- AVRO-4228: negative *array* block counts in `arrayNext()`;
- AVRO-4278/4294: array/map allocation and zero-byte-element limits;
- PR #3623: negative OCF *byte size* validation in the C implementation;
- AVRO-3587: a 2022 compressed-block heap-buffer-overflow in
`readDataBlock()`.
None validates the C++ OCF object count before `hasMore()` and `decr()`.