#![no_main] //! Console-command extraction decodes every function in the image and does arithmetic on values the //! FILE controls at every step: `lea` displacements are added to tracked register contents, member //! offsets are added to a symbolic base, and the accessor window walks `arg3 + k*8` looking for a stored //! pointer. A crafted (or truncated) `.so` can make any of those wrap, point outside every section, or //! nest arbitrarily deep — and the reader must answer with fewer commands, never a panic. //! //! It also exercises the two indirections that resolve a callback (a static object's first virtual, and //! a constructor-stored member) against pointers the file chose, which is the same untrusted-chase shape //! the Valve table readers had to be hardened for. //! //! CONVAR extraction rides the same pass and is fuzzed here with it. It adds two things worth attacking: //! a per-registrar delegation walk (bounded call/tail-jump decoding at a file-chosen address) and a //! statistical argument-slot choice, both driven entirely by bytes the file controls. use libfuzzer_sys::fuzz_target; use source2rosetta::concmd; use source2rosetta::elf::CodeImage; fuzz_target!(|data: &[u8]| { let Ok(img) = CodeImage::from_bytes(data.to_vec()) else { return; }; let cmds = concmd::console_commands(&img); for c in &cmds { // A recovered handler is only ever accepted because it lands in executable code, so the reader // must never hand back one that does not — a caller treats this as a locator. assert!( img.is_code(c.handler), "a non-executable handler was recorded for {:?}", c.name ); // The name gate is what stops prose and format strings being read as commands. assert!( !c.name.is_empty() && c.name.len() <= 64, "an implausible command name was recorded: {:?}", c.name ); // Flag decoding is a pure bit test and must stay within the bits it claims to know. let named = concmd::flag_names(c.flags); assert!(named.len() <= 12, "more flag names than there are flag bits"); let _ = (c.description.len(), c.form.describe(), c.flags); } // ---- ConVars: same pass, different registrar test ---- let cvs = concmd::convars(&img, "server"); for c in &cvs { // The name gate is the only thing separating a convar registration from any other call that // happens to pass a string, so it must hold on every row. assert!( !c.name.is_empty() && c.name.len() <= 64, "an implausible convar name was recorded: {:?}", c.name ); // Flags are optional (a registrar with no identifiable slot reports none), but when present the // decode is a pure bit test over a table of 9 and cannot exceed it. if !c.flags_raw.is_empty() { let raw = u64::from_str_radix(c.flags_raw.trim_start_matches("0x"), 16) .expect("flags_raw is written as hex by this reader"); assert!(raw <= u64::from(u32::MAX), "a convar flags word exceeded 32 bits"); assert_eq!( c.flags.len(), concmd::convar_flag_names(raw).len(), "decoded flag names disagree with the raw word for {:?}", c.name ); } else { assert!(c.flags.is_empty(), "flag names without a raw word for {:?}", c.name); } let _ = (c.description.len(), c.addr.len(), c.library.len()); } // ConVars are deduped on (name, object address) for the same reason commands are. let mut cseen: Vec<(&str, &str)> = cvs.iter().map(|c| (c.name.as_str(), c.addr.as_str())).collect(); let cbefore = cseen.len(); cseen.sort_unstable(); cseen.dedup(); assert_eq!(cbefore, cseen.len(), "a duplicate (name, object) convar survived"); // Commands are deduped on (name, address), so no pair may survive twice. let mut seen: Vec<(&str, u64)> = cmds.iter().map(|c| (c.name.as_str(), c.handler)).collect(); let before = seen.len(); seen.sort_unstable(); seen.dedup(); assert_eq!(before, seen.len(), "a duplicate (name, handler) survived"); });