Skip to content

Commit f51d186

Browse files
fix: patch Huffman OOB read, eliminate arithmetic decode copy, optimize encoders (#11)
Security fix: - Fix out-of-bounds read in Huffman decode table build: when a corrupt or single-child tree caused cur < 0, the `e.next = cur` assignment at loop exit overwrote the earlier `e.next = root` safety reset, producing a negative node index. On crafted input with a single-symbol frequency table (e.g. empty input), this led to `table[-1]` access (undefined behavior). Now guarded with a `corrupt` flag so the root reset survives. Optimization: - Eliminate O(n) payload copy in arithmetic decode: BitReader now accepts (const uint8_t*, size_t) in addition to const vector&, letting the decoder read directly from the input buffer at the payload offset without allocating a temporary vector. - Add Vec::reserve() to arithmetic and RLE encoders to reduce reallocation overhead (matching huffman/range encoders). Test coverage: - Add all_same_byte.bin corpus file (4096 zero bytes) to exercise RLE single-run and Huffman single-symbol edge cases, included in the CLI smoke test default corpus. Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 158b60e commit f51d186

6 files changed

Lines changed: 16 additions & 9 deletions

File tree

algorithms/arithmetic/cpp/main.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ std::vector<uint8_t> arithmetic_encode_buffer(const std::vector<uint8_t>& input)
147147
}
148148

149149
std::vector<uint8_t> out;
150+
out.reserve(input.size() + compresskit::INITIAL_ENCODE_OVERHEAD);
150151
compresskit::write_frequency_header(out, compresskit::ARITHMETIC_MAGIC, freq);
151152

152153
compresskit::BitWriter writer;
@@ -176,11 +177,10 @@ std::vector<uint8_t> arithmetic_decode_buffer(const std::vector<uint8_t>& input)
176177
throw std::runtime_error("arithmetic: invalid frequency table");
177178
}
178179

179-
std::vector<uint8_t> payload(input.begin() + pos, input.end());
180-
compresskit::BitReader reader(payload);
180+
std::vector<uint8_t> out;
181+
compresskit::BitReader reader(input.data() + pos, input.size() - pos);
181182
ArithmeticDecoder decoder(reader);
182183

183-
std::vector<uint8_t> out;
184184
for (;;) {
185185
uint32_t sym = decoder.decode_symbol(cumulative);
186186
if (sym == compresskit::EOF_SYMBOL) {

algorithms/huffman/cpp/main.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,21 +124,22 @@ void build_decode_table(const std::vector<Node>& nodes, int32_t root,
124124
for (uint32_t b = 0; b < compresskit::BYTE_VALUES; ++b) {
125125
DecodeEntry& e = table[node][b];
126126
int32_t cur = node;
127+
bool corrupt = false;
127128
for (int bit = compresskit::BITS_PER_BYTE - 1; bit >= 0; --bit) {
128129
int v = (b >> bit) & 1;
129130
cur = (v == 0) ? nodes[cur].left : nodes[cur].right;
130131
if (cur < 0) {
131132
// Corrupt stream during table build: shouldn't happen for valid trees.
132133
e.count = 0;
133-
e.next = root;
134+
corrupt = true;
134135
break;
135136
}
136137
if (is_leaf(nodes, cur)) {
137138
e.symbols[e.count++] = nodes[cur].symbol;
138139
cur = root;
139140
}
140141
}
141-
e.next = cur;
142+
e.next = corrupt ? root : cur;
142143
}
143144
}
144145
}

algorithms/rle/cpp/main.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
std::vector<uint8_t> rle_encode_buffer(const std::vector<uint8_t>& input) {
1717
std::vector<uint8_t> out;
18+
out.reserve(input.size() / 8 + compresskit::MAGIC_SIZE + compresskit::RLE_PAIR_SIZE);
1819
compresskit::write_magic(out, compresskit::RLE_MAGIC);
1920

2021
if (input.empty()) {

algorithms/shared/cpp/include/compresskit/bit_io.hpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ class BitWriter {
4444
// MSB-first bit reader. Returns 0 for bits read past the end of the stream.
4545
class BitReader {
4646
public:
47-
explicit BitReader(const std::vector<uint8_t>& data) : data_(data) {}
47+
explicit BitReader(const std::vector<uint8_t>& data) : data_(data.data()), size_(data.size()) {}
48+
BitReader(const uint8_t* data, std::size_t size) : data_(data), size_(size) {}
4849
int read_bit() {
49-
if (byte_pos_ >= data_.size()) {
50+
if (byte_pos_ >= size_) {
5051
return 0;
5152
}
5253
int bit = (data_[byte_pos_] >> ((BITS_PER_BYTE - 1) - bit_pos_)) & 1;
@@ -56,10 +57,11 @@ class BitReader {
5657
}
5758
return bit;
5859
}
59-
bool eof() const { return byte_pos_ >= data_.size(); }
60+
bool eof() const { return byte_pos_ >= size_; }
6061

6162
private:
62-
const std::vector<uint8_t>& data_;
63+
const uint8_t* data_;
64+
std::size_t size_;
6365
std::size_t byte_pos_ = 0;
6466
int bit_pos_ = 0;
6567
};

tests/conformance/run_cli_smoke.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
ROOT / "tests/data/empty.bin",
2626
ROOT / "tests/data/single_byte.bin",
2727
ROOT / "tests/data/alternating.bin",
28+
ROOT / "tests/data/all_same_byte.bin",
2829
ROOT / "tests/data/small_dictionary_like.bin",
2930
)
3031

tests/gen_testdata.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# - empty.bin 空文件
1313
# - single_byte.bin 单字节边界样本
1414
# - alternating.bin 交替字节模式
15+
# - all_same_byte.bin 全相同字节样本(RLE/Huffman 边界情况)
1516
# - small_dictionary_like.bin 小型重复词典风格样本
1617

1718
ROOT = Path(__file__).resolve().parent.parent
@@ -87,6 +88,7 @@ def main():
8788
generate_literal_file(DATA_DIR / "empty.bin", b"")
8889
generate_literal_file(DATA_DIR / "single_byte.bin", b"\x00")
8990
generate_literal_file(DATA_DIR / "alternating.bin", (b"\xAA\x55" * 512))
91+
generate_literal_file(DATA_DIR / "all_same_byte.bin", (b"\x00" * 4096))
9092
generate_literal_file(
9193
DATA_DIR / "small_dictionary_like.bin",
9294
(b"compresskit-dict-alpha\n" * 128)

0 commit comments

Comments
 (0)