Skip to main content

ruso_runtime/runtime/
disasm.rs

1//! Human-readable bytecode disassembly.
2
3use std::fmt::Write as _;
4
5use crate::contract::{
6    CmpOp, CmpValue, EvidenceKind, ExtractSource, FieldKind, MatchPredicate, QualifiedMatch,
7};
8use crate::runtime::binary;
9use crate::runtime::bytecode::{BytecodeProgram, Instr};
10use crate::runtime::spec::ProbeKind;
11
12pub fn format_human(bytecode: &BytecodeProgram) -> String {
13    let mut out = String::new();
14
15    writeln!(out, ";; metadata").ok();
16    format_metadata(&mut out, &bytecode.spec.metadata);
17    writeln!(out).ok();
18
19    writeln!(out, ";; probes ({})", bytecode.spec.probes.len()).ok();
20    let mut probe_names: Vec<_> = bytecode.spec.probes.keys().collect();
21    probe_names.sort();
22    for name in probe_names {
23        let kind = &bytecode.spec.probes[name];
24        writeln!(out, ";;   probe {name}: {}", format_probe_kind(kind)).ok();
25    }
26    writeln!(out).ok();
27
28    if !bytecode.strings.is_empty() {
29        writeln!(out, ";; strings").ok();
30        for (idx, value) in bytecode.strings.iter().enumerate() {
31            writeln!(out, ";;   [{idx}] {value:?}").ok();
32        }
33        writeln!(out).ok();
34    }
35
36    if !bytecode.matchers.is_empty() {
37        writeln!(out, ";; matchers").ok();
38        for (idx, matcher) in bytecode.matchers.iter().enumerate() {
39            writeln!(out, ";;   [{idx}] {}", format_matcher(matcher)).ok();
40        }
41        writeln!(out).ok();
42    }
43
44    if !bytecode.extracts.is_empty() {
45        writeln!(out, ";; extracts").ok();
46        for (idx, source) in bytecode.extracts.iter().enumerate() {
47            writeln!(out, ";;   [{idx}] {}", format_extract(source)).ok();
48        }
49        writeln!(out).ok();
50    }
51
52    if !bytecode.evidence.is_empty() {
53        writeln!(out, ";; evidence").ok();
54        for (idx, kind) in bytecode.evidence.iter().enumerate() {
55            writeln!(out, ";;   [{idx}] {}", format_evidence(kind)).ok();
56        }
57        writeln!(out).ok();
58    }
59
60    writeln!(out, ";; code ({} instructions)", bytecode.code.len()).ok();
61    for (pc, instr) in bytecode.code.iter().enumerate() {
62        writeln!(out, "  pc {pc:>3}: {}", format_instr(instr, bytecode)).ok();
63    }
64
65    out
66}
67
68fn format_metadata(out: &mut String, metadata: &crate::runtime::spec::CheckMetadata) {
69    if let Some(name) = &metadata.name {
70        writeln!(out, ";;   name: {name:?}").ok();
71    }
72    if let Some(description) = &metadata.description {
73        writeln!(out, ";;   description: {description:?}").ok();
74    }
75    if let Some(impact) = &metadata.impact {
76        writeln!(out, ";;   impact: {impact:?}").ok();
77    }
78    if let Some(severity) = &metadata.severity {
79        writeln!(out, ";;   severity: {}", severity.as_str()).ok();
80    }
81    if let Some(author) = &metadata.author {
82        writeln!(out, ";;   author: {author:?}").ok();
83    }
84    for cve in &metadata.cve {
85        writeln!(out, ";;   cve: {cve:?}").ok();
86    }
87    for cwe in &metadata.cwe {
88        writeln!(out, ";;   cwe: {cwe:?}").ok();
89    }
90    for reference in &metadata.references {
91        writeln!(out, ";;   references: {reference:?}").ok();
92    }
93    for cvss in &metadata.cvss {
94        writeln!(out, ";;   cvss: {cvss:?}").ok();
95    }
96    for score in &metadata.cvss_score {
97        writeln!(out, ";;   cvss_score: {score:?}").ok();
98    }
99    if let Some(mitigation) = &metadata.mitigation {
100        writeln!(out, ";;   mitigation: {mitigation:?}").ok();
101    }
102    for tag in &metadata.tags {
103        writeln!(out, ";;   tag: {tag:?}").ok();
104    }
105    if let Some(version) = &metadata.version {
106        writeln!(out, ";;   version: {version:?}").ok();
107    }
108    if let Some(family) = &metadata.family {
109        writeln!(out, ";;   family: {family:?}").ok();
110    }
111}
112
113fn format_probe_kind(kind: &ProbeKind) -> String {
114    match kind {
115        ProbeKind::Http(spec) => {
116            format!("http {} {}", format_http_method(&spec.method), spec.path)
117        }
118        ProbeKind::Dns(spec) => format_socket_probe("dns", spec),
119        ProbeKind::Tcp(spec) => format_socket_probe("tcp", spec),
120        ProbeKind::Udp(spec) => format_socket_probe("udp", spec),
121    }
122}
123
124fn format_socket_probe(label: &str, spec: &crate::runtime::spec::SocketProbeSpec) -> String {
125    let mut line = format!("{label} host={:?}", spec.host);
126    if let Some(port) = spec.port {
127        line.push_str(&format!(" port={port}"));
128    }
129    if let Some(payload) = &spec.payload {
130        let is_text = payload.iter().all(|b| {
131            b.is_ascii_graphic() || *b == b' ' || *b == b'\r' || *b == b'\n' || *b == b'\t'
132        });
133        let shown = if is_text {
134            // Decode lossy as text — `format!("{:?}", Vec<u8>)` would
135            // print "[80, 73, 78, 71]" instead of "\"PING\"".
136            let text = String::from_utf8_lossy(payload);
137            format!("{text:?}")
138        } else {
139            format!("0x{}", crate::runtime::binary::bytes_to_hex(payload))
140        };
141        line.push_str(&format!(" payload={shown}"));
142    }
143    if label == "dns" && spec.is_dns_resolver_mode() {
144        line.push_str(" (resolver)");
145    }
146    line
147}
148
149fn format_http_method(method: &crate::contract::HttpMethod) -> &'static str {
150    use crate::contract::HttpMethod;
151    match method {
152        HttpMethod::Get => "GET",
153        HttpMethod::Post => "POST",
154        HttpMethod::Put => "PUT",
155        HttpMethod::Patch => "PATCH",
156        HttpMethod::Delete => "DELETE",
157        HttpMethod::Head => "HEAD",
158        HttpMethod::Options => "OPTIONS",
159    }
160}
161
162fn format_matcher(matcher: &QualifiedMatch) -> String {
163    let field = match &matcher.field.kind {
164        FieldKind::Status => "status".to_string(),
165        FieldKind::Body => "body".to_string(),
166        FieldKind::Header(name) => format!("header({name:?})"),
167        FieldKind::ResponseTime => "response_time".to_string(),
168        FieldKind::ResponseSize => "response_size".to_string(),
169        FieldKind::Answer => "answer".to_string(),
170        FieldKind::Banner => "banner".to_string(),
171        FieldKind::Response => "response".to_string(),
172    };
173    format!(
174        "{}.{} {}",
175        matcher.field.target,
176        field,
177        format_predicate(&matcher.predicate)
178    )
179}
180
181fn format_predicate(predicate: &MatchPredicate) -> String {
182    match predicate {
183        MatchPredicate::Compare { op, value } => {
184            format!("{} {}", format_cmp_op(*op), format_cmp_value(value))
185        }
186        MatchPredicate::Contains(text) => format!("contains {text:?}"),
187        MatchPredicate::NotContains(text) => format!("not_contains {text:?}"),
188        MatchPredicate::Regex(pattern) => format!("regex {pattern:?}"),
189    }
190}
191
192fn format_cmp_op(op: CmpOp) -> &'static str {
193    match op {
194        CmpOp::Eq => "==",
195        CmpOp::Ne => "!=",
196        CmpOp::Lt => "<",
197        CmpOp::Gt => ">",
198        CmpOp::Le => "<=",
199        CmpOp::Ge => ">=",
200    }
201}
202
203fn format_cmp_value(value: &CmpValue) -> String {
204    match value {
205        CmpValue::Number(n) => n.to_string(),
206        CmpValue::String(s) => format!("{s:?}"),
207        CmpValue::Duration(d) => d.clone(),
208    }
209}
210
211fn format_extract(source: &ExtractSource) -> String {
212    match source {
213        ExtractSource::Body { target, regex } => match regex {
214            Some(pattern) => format!("body from {target} regex {pattern:?}"),
215            None => format!("body from {target}"),
216        },
217        ExtractSource::Header { target, name } => {
218            format!("header {name:?} from {target}")
219        }
220    }
221}
222
223fn format_evidence(kind: &EvidenceKind) -> String {
224    let re = |p: &Option<String>| match p {
225        Some(pat) => format!(" regex {pat:?}"),
226        None => String::new(),
227    };
228    match kind {
229        EvidenceKind::Body { target, pattern } => format!("{target}.body{}", re(pattern)),
230        EvidenceKind::Response { target, pattern } => format!("{target}.response{}", re(pattern)),
231        EvidenceKind::Header {
232            target,
233            name,
234            pattern,
235        } => format!("{target}.header {name:?}{}", re(pattern)),
236    }
237}
238
239fn format_instr(instr: &Instr, bytecode: &BytecodeProgram) -> String {
240    let str_at = |idx: u32| -> String {
241        bytecode
242            .strings
243            .get(idx as usize)
244            .map(|s| format!("{s:?}"))
245            .unwrap_or_else(|| format!("#{idx}?"))
246    };
247    // Use `.get()` rather than direct slicing so corrupt-but-decodable
248    // bytecode (start/len pointing past the string pool) cannot panic the
249    // disassembler — important because `ruso disasm` is reachable from
250    // untrusted `.rbc` files.
251    let string_span = |start: u32, len: u16| -> String {
252        let start = start as usize;
253        let end = start.saturating_add(len as usize);
254        match bytecode.strings.get(start..end) {
255            Some(slice) => slice
256                .iter()
257                .map(|value| format!("{value:?}"))
258                .collect::<Vec<_>>()
259                .join(", "),
260            None => format!("<oob {start}..{end}>"),
261        }
262    };
263
264    match instr {
265        Instr::Set { name, value } => {
266            format!("Set name={} value={}", str_at(*name), str_at(*value))
267        }
268        Instr::SetList { name, start, len } => {
269            format!(
270                "SetList name={} values=[{}]",
271                str_at(*name),
272                string_span(*start, *len)
273            )
274        }
275        Instr::Send { probe, payload } => {
276            if let Some(id) = payload {
277                format!(
278                    "Send {} payload=[{}]",
279                    str_at(*probe),
280                    bytecode
281                        .payloads
282                        .get(*id as usize)
283                        .map(|p| binary::bytes_to_hex(p))
284                        .unwrap_or_else(|| format!("#{id}?"))
285                )
286            } else {
287                format!("Send {}", str_at(*probe))
288            }
289        }
290        Instr::Match(matcher) => format!("Match [{}]", matcher),
291        Instr::MatchAll { start, len } => format!("MatchAll [{start}..{}]", start + *len as u32),
292        Instr::MatchAny { start, len } => format!("MatchAny [{start}..{}]", start + *len as u32),
293        Instr::Assert(matcher) => format!("Assert [{}]", matcher),
294        Instr::Extract { name, source } => {
295            format!("Extract name={} source=[{}]", str_at(*name), source)
296        }
297        Instr::IfMatch { matcher, else_pc } => {
298            format!("IfMatch [{}] else_pc={else_pc}", matcher)
299        }
300        Instr::ForList {
301            item,
302            start,
303            len,
304            end_pc,
305        } => format!(
306            "ForList item={} values=[{}] end_pc={end_pc}",
307            str_at(*item),
308            string_span(*start, *len)
309        ),
310        Instr::ForVar { item, list, end_pc } => {
311            format!(
312                "ForVar item={} list={} end_pc={end_pc}",
313                str_at(*item),
314                str_at(*list)
315            )
316        }
317        Instr::LoopBack => "LoopBack".into(),
318        Instr::Break => "Break".into(),
319        Instr::Save { from, to } => {
320            format!("Save {} as {}", str_at(*from), str_at(*to))
321        }
322        Instr::Evidence(kind) => format!("Evidence [{}]", kind),
323        Instr::Retry { probe, count } => {
324            format!("Retry {} count={count}", str_at(*probe))
325        }
326        Instr::RetryDelay(value) => format!("RetryDelay {}", str_at(*value)),
327        Instr::Sleep(value) => format!("Sleep {}", str_at(*value)),
328        Instr::Stop => "Stop".into(),
329        Instr::Fail => "Fail".into(),
330        Instr::Continue => "Continue".into(),
331        Instr::Exit => "Exit".into(),
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::runtime::bytecode::Instr;
339    use crate::runtime::spec::{CheckMetadata, ProgramSpec};
340
341    fn empty_bytecode() -> BytecodeProgram {
342        BytecodeProgram {
343            spec: ProgramSpec {
344                probes: Default::default(),
345                metadata: CheckMetadata::default(),
346            },
347            code: vec![],
348            strings: vec![],
349            payloads: vec![],
350            matchers: vec![],
351            extracts: vec![],
352            evidence: vec![],
353        }
354    }
355
356    #[test]
357    fn out_of_bounds_string_span_does_not_panic() {
358        // Crafted (corrupt) bytecode where ForList claims a string span
359        // beyond the actual pool. Pre-fix this would panic in the
360        // disassembler — now it should render a sentinel.
361        let bytecode = BytecodeProgram {
362            code: vec![Instr::ForList {
363                item: 99,
364                start: 99,
365                len: 5,
366                end_pc: 0,
367            }],
368            ..empty_bytecode()
369        };
370        let out = format_human(&bytecode);
371        assert!(out.contains("<oob"), "expected oob sentinel, got:\n{out}");
372    }
373}