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