mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
labels: harden class_reader/jar as first-class .class format readers
Treat Java bytecode as a documented format we READ (never execute), a peer of mpls/clpi. The class reader's bounds checking was already sound (typed Reader with checked u8/u16/u32/u64 + slice, saturating switch arithmetic, iterator halt-on-truncation); this adds the untrusted-input entry points the never-panic harness was missing and the malformed-input regression tests the format warranted. - harness: add class_reader (magic CAFEBABE) and jar (zip PK header) sweep targets. The class_reader target also drives the accessors a label parser exercises (this_class_name, member Code walk, cp_index, member_ref, load_constant_display) so their indexing is fuzzed, not just the structural parse. The jar target opens the sweep bytes as a zip and walks every .class through for_each_class exactly as deluxe/dbp do. - class_reader tests: oversized constant_pool_count and interfaces_count reject with UnexpectedEof (u16 counts keep with_capacity bounded); out-of-range cp indices return None from every accessor; a Code attribute whose declared code_length runs past its body is rejected.
This commit is contained in:
@@ -189,6 +189,50 @@ fn udf_name_parse_never_panics() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_reader_parse_never_panics() {
|
||||
// Java `.class` files are a documented format we READ (never execute) to
|
||||
// recover BD-J stream labels — a peer of mpls/clpi here. Every count,
|
||||
// length and constant-pool index in the file is attacker-controlled disc
|
||||
// metadata, so the parser must reject malformed input with `Err`, never
|
||||
// panic. Magic is the class-file magic `0xCAFEBABE`.
|
||||
sweep("class_reader", &[0xCA, 0xFE, 0xBA, 0xBE], |b| {
|
||||
if let Ok(class) = crate::labels::class_reader::ClassFile::parse(b) {
|
||||
// Exercise the accessors a label parser drives, so their indexing
|
||||
// is fuzzed too, not just the structural parse.
|
||||
let _ = class.this_class_name();
|
||||
let _ = class.super_class_name();
|
||||
for m in &class.methods {
|
||||
let _ = class.member_name(m);
|
||||
if let Some(code) = m.code(&class.constant_pool) {
|
||||
for insn in code.instructions() {
|
||||
let _ = insn.cp_index();
|
||||
if let Some(idx) = insn.cp_index() {
|
||||
let _ = class.constant_pool.get(idx);
|
||||
let _ = class.constant_pool.member_ref(idx);
|
||||
let _ = class.constant_pool.load_constant_display(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jar_class_walk_never_panics() {
|
||||
// The jar layer opens a `/BDMV/JAR/<x>.jar` (a zip) and walks every
|
||||
// `.class` inside it, handing each to the class reader. Feed the sweep's
|
||||
// bytes as a zip archive; when they parse as one, walk the classes exactly
|
||||
// as `deluxe`/`dbp` do. Neither the zip open nor the class walk may panic.
|
||||
// Magic is the zip local-file-header signature `PK\x03\x04`.
|
||||
sweep("jar", &[0x50, 0x4B, 0x03, 0x04], |b| {
|
||||
if let Ok(mut archive) = zip::ZipArchive::new(std::io::Cursor::new(b.to_vec())) {
|
||||
crate::labels::jar::for_each_class(&mut archive, |_name, _class| {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ps_demuxer_feed_never_panics() {
|
||||
// Stateful, unlike the others: the demuxer carries a buffer across feeds, so
|
||||
|
||||
@@ -1723,4 +1723,75 @@ mod tests {
|
||||
// total = 1 + 3 + 8 (default/npairs) + 24 = 36
|
||||
assert_eq!(instruction_size(&code, 0), Some(36));
|
||||
}
|
||||
|
||||
// ── Malformed-input hardening: every count/length/index is attacker- ──────
|
||||
// controlled, so an oversized or lying field must yield Err/None, never a
|
||||
// panic or an unbounded allocation.
|
||||
|
||||
/// A `constant_pool_count` far larger than the data behind it must fail
|
||||
/// cleanly (running out of bytes), not pre-allocate gigabytes or panic. The
|
||||
/// count is a u16, so `Vec::with_capacity` is bounded at 65535 regardless.
|
||||
#[test]
|
||||
fn parse_rejects_oversized_constant_pool_count() {
|
||||
let bytes = [
|
||||
0xCA, 0xFE, 0xBA, 0xBE, // magic
|
||||
0x00, 0x00, 0x00, 0x00, // minor / major
|
||||
0xFF, 0xFF, // constant_pool_count = 65535, but no entries follow
|
||||
];
|
||||
assert!(matches!(
|
||||
ClassFile::parse(&bytes),
|
||||
Err(Error::UnexpectedEof { .. })
|
||||
));
|
||||
}
|
||||
|
||||
/// An oversized `interfaces_count` / `fields_count` with no data behind it
|
||||
/// must fail on the first missing element, not panic.
|
||||
#[test]
|
||||
fn parse_rejects_oversized_interface_count() {
|
||||
let bytes = [
|
||||
0xCA, 0xFE, 0xBA, 0xBE, // magic
|
||||
0x00, 0x00, 0x00, 0x00, // minor / major
|
||||
0x00, 0x01, // constant_pool_count = 1 (no entries)
|
||||
0x00, 0x00, // access_flags
|
||||
0x00, 0x00, // this_class
|
||||
0x00, 0x00, // super_class
|
||||
0xFF, 0xFF, // interfaces_count = 65535, none follow
|
||||
];
|
||||
assert!(matches!(
|
||||
ClassFile::parse(&bytes),
|
||||
Err(Error::UnexpectedEof { .. })
|
||||
));
|
||||
}
|
||||
|
||||
/// Out-of-range constant-pool indices resolve to `None` from every
|
||||
/// accessor a label parser drives — no slice panic.
|
||||
#[test]
|
||||
fn constant_pool_out_of_range_index_is_none_everywhere() {
|
||||
let pool = sample_pool();
|
||||
let oob = 9999u16;
|
||||
assert!(pool.get(oob).is_none());
|
||||
assert!(pool.utf8(oob).is_none());
|
||||
assert!(pool.class_name(oob).is_none());
|
||||
assert!(pool.string(oob).is_none());
|
||||
assert!(pool.integer(oob).is_none());
|
||||
assert!(pool.member_ref(oob).is_none());
|
||||
assert!(pool.load_constant_display(oob).is_none());
|
||||
}
|
||||
|
||||
/// A `Code` attribute whose declared `code_length` runs past the attribute
|
||||
/// body must be rejected, not read out of bounds.
|
||||
#[test]
|
||||
fn code_attribute_rejects_length_past_its_body() {
|
||||
// max_stack(2) + max_locals(2) + code_length(4) then a code_length that
|
||||
// exceeds the remaining bytes.
|
||||
let mut info = Vec::new();
|
||||
info.extend_from_slice(&0u16.to_be_bytes()); // max_stack
|
||||
info.extend_from_slice(&0u16.to_be_bytes()); // max_locals
|
||||
info.extend_from_slice(&0xFFFF_FFFFu32.to_be_bytes()); // code_length: absurd
|
||||
info.extend_from_slice(&[0x00, 0x01]); // only 2 code bytes present
|
||||
assert!(matches!(
|
||||
parse_code_attribute(&info),
|
||||
Err(Error::UnexpectedEof { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user