Skip to main content

sbpf_disassembler/
program.rs

1use {
2    crate::{
3        elf_header::{E_MACHINE, E_MACHINE_SBPF, ELFHeader},
4        errors::DisassemblerError,
5        program_header::ProgramHeader,
6        relocation::Relocation,
7        rodata::RodataSection,
8        section_header::SectionHeader,
9        section_header_entry::SectionHeaderEntry,
10    },
11    either::Either,
12    object::{Endianness, read::elf::ElfFile64},
13    sbpf_common::{
14        errors::SBPFError, inst_param::Number, instruction::Instruction, opcode::Opcode,
15    },
16    serde::{Deserialize, Serialize},
17    std::collections::{BTreeSet, HashMap},
18};
19
20/// Outcome of an error-tolerant operation, the value `T` plus every error found while producing it.
21#[derive(Debug)]
22#[must_use]
23pub struct Parsed<T> {
24    pub value: T,
25    pub errors: Vec<DisassemblerError>,
26}
27
28impl<T> Parsed<T> {
29    /// Collapse to strict semantics where any error becomes failure.
30    /// used in places where the errors are unrecoverable
31    pub fn into_strict(self) -> Result<T, Vec<DisassemblerError>> {
32        if self.errors.is_empty() {
33            Ok(self.value)
34        } else {
35            Err(self.errors)
36        }
37    }
38}
39
40#[derive(Debug)]
41pub struct Disassembly {
42    pub instructions: Vec<Either<Instruction, DisassemblerError>>,
43    pub rodata: Option<RodataSection>,
44    pub entrypoint: Option<usize>,
45}
46
47pub type DisassembleResult = Result<Parsed<Disassembly>, Vec<DisassemblerError>>;
48
49#[derive(Debug, Serialize, Deserialize)]
50pub struct Program {
51    pub elf_header: ELFHeader,
52    pub program_headers: Vec<ProgramHeader>,
53    pub section_headers: Vec<SectionHeader>,
54    pub section_header_entries: Vec<SectionHeaderEntry>,
55    pub relocations: Vec<Relocation>,
56}
57
58impl Program {
59    pub fn from_bytes(b: &[u8]) -> Result<Self, Vec<DisassemblerError>> {
60        let mut errors = Vec::new();
61
62        let elf_file = match ElfFile64::<Endianness>::parse(b) {
63            Ok(elf_file) => elf_file,
64            Err(source) => {
65                errors.push(DisassemblerError::InvalidElfFile {
66                    first_bytes: b.get(..16).unwrap_or(b).to_vec(),
67                    source,
68                });
69                // Nothing to parse headers from.
70                return Err(errors);
71            }
72        };
73
74        // Parse elf header.
75        let elf_header = ELFHeader::from_elf_file(&elf_file)?;
76
77        // Parse program headers.
78        let program_headers = ProgramHeader::from_elf_file(&elf_file)?;
79
80        // Parse section headers and section header entries.
81        let (section_headers, section_header_entries) = SectionHeader::from_elf_file(&elf_file)?;
82
83        // Parse relocations.
84        let relocations = Relocation::from_elf_file(&elf_file)?;
85
86        // v3 binaries omit the section header table; reconstruct the .text and
87        // .rodata section views from the program (segment) headers so the rest
88        // of the disassembler can locate them by name.
89        let (section_headers, section_header_entries) = if section_header_entries.is_empty() {
90            Self::synthesize_sections_from_segments(b, &program_headers).map_err(|e| vec![e])?
91        } else {
92            (section_headers, section_header_entries)
93        };
94
95        Ok(Self {
96            elf_header,
97            program_headers,
98            section_headers,
99            section_header_entries,
100            relocations,
101        })
102    }
103
104    /// Reconstruct `.text` and `.rodata` section views from loadable program
105    /// segments. Used for v3 binaries, which carry no section header table:
106    /// the executable segment becomes `.text` and the read-only,
107    /// non-executable segment becomes `.rodata`. The synthesized section
108    /// headers mirror what the assembler used to emit (`sh_addr == sh_offset ==
109    /// file offset`) so downstream offset resolution is unchanged.
110    fn synthesize_sections_from_segments(
111        data: &[u8],
112        program_headers: &[ProgramHeader],
113    ) -> Result<(Vec<SectionHeader>, Vec<SectionHeaderEntry>), DisassemblerError> {
114        use crate::{
115            program_header::{PF_X, ProgramType},
116            section_header::SectionHeaderType,
117        };
118
119        let segment_bytes = |offset: u64, size: u64| -> Vec<u8> {
120            let start = offset as usize;
121            let end = start.saturating_add(size as usize).min(data.len());
122            if start >= data.len() {
123                Vec::new()
124            } else {
125                data[start..end].to_vec()
126            }
127        };
128
129        let make_header = |offset: u64, size: u64, executable: bool| SectionHeader {
130            sh_name: 0,
131            sh_type: SectionHeaderType::SHT_PROGBITS,
132            sh_flags: if executable { 0x6 } else { 0x2 }, // SHF_ALLOC (+ SHF_EXECINSTR)
133            sh_addr: offset,
134            sh_offset: offset,
135            sh_size: size,
136            sh_link: 0,
137            sh_info: 0,
138            sh_addralign: if executable { 8 } else { 1 },
139            sh_entsize: 0,
140        };
141
142        let mut headers = Vec::new();
143        let mut entries = Vec::new();
144
145        let is_load = |ph: &&ProgramHeader| matches!(ph.p_type, ProgramType::PT_LOAD);
146        let is_exec = |ph: &ProgramHeader| ph.p_flags.0 & PF_X as u32 == PF_X as u32;
147
148        // .rodata: read-only, non-executable loadable segment (if present).
149        if let Some(ph) = program_headers
150            .iter()
151            .filter(is_load)
152            .find(|ph| !is_exec(ph))
153        {
154            headers.push(make_header(ph.p_offset, ph.p_filesz, false));
155            entries.push(SectionHeaderEntry::new(
156                ".rodata\0".to_string(),
157                ph.p_offset as usize,
158                segment_bytes(ph.p_offset, ph.p_filesz),
159            )?);
160        }
161
162        // .text: executable loadable segment.
163        if let Some(ph) = program_headers
164            .iter()
165            .filter(is_load)
166            .find(|ph| is_exec(ph))
167        {
168            headers.push(make_header(ph.p_offset, ph.p_filesz, true));
169            entries.push(SectionHeaderEntry::new(
170                ".text\0".to_string(),
171                ph.p_offset as usize,
172                segment_bytes(ph.p_offset, ph.p_filesz),
173            )?);
174        }
175
176        Ok((headers, entries))
177    }
178
179    pub fn to_ixs(self) -> DisassembleResult {
180        self.into_ixs_inner(true)
181    }
182
183    pub fn to_ixs_raw(self) -> DisassembleResult {
184        self.into_ixs_inner(false)
185    }
186
187    fn into_ixs_inner(self, resolve_offsets: bool) -> DisassembleResult {
188        // Find and populate instructions for the .text section
189        let text_section = self
190            .section_header_entries
191            .iter()
192            .find(|e| e.label.eq(".text\0"))
193            .ok_or_else(|| {
194                vec![DisassemblerError::MissingTextSection {
195                    sections: self
196                        .section_header_entries
197                        .iter()
198                        .map(|e| e.label.trim_end_matches('\0').to_string())
199                        .collect(),
200                }]
201            })?;
202
203        let mut errors = Vec::new();
204
205        let text_section_offset = text_section.offset as u64;
206
207        // Build syscall map
208        let syscall_map = self.build_syscall_map(text_section_offset);
209
210        let data = &text_section.data;
211        if !data.len().is_multiple_of(8) {
212            errors.push(DisassemblerError::InvalidDataLength(data.len()));
213        }
214
215        let is_sbpf_v2 =
216            self.elf_header.e_flags == 0x02 && self.elf_header.e_machine == E_MACHINE_SBPF;
217        let is_sbpf_v3 = self.elf_header.e_flags == 0x03 && self.elf_header.e_machine == E_MACHINE;
218
219        // Get rodata info
220        let rodata_info = self.get_rodata_info();
221        let (rodata_base, rodata_end) = rodata_info
222            .as_ref()
223            .map(|(d, addr)| (*addr, *addr + d.len() as u64))
224            .unwrap_or((0, 0));
225
226        // Parse instructions and build slot mappings
227        let mut ixs: Vec<Either<Instruction, DisassemblerError>> = Vec::new();
228        let mut idx_to_slot: Vec<usize> = Vec::new();
229        let mut pos: usize = 0;
230        let mut slot: usize = 0;
231
232        while pos < data.len() {
233            let remaining = &data[pos..];
234            if remaining.len() < 8 {
235                break;
236            }
237
238            // lddw (0x18) is the only 16-byte instruction; sbpf-common's
239            // decoder asserts on shorter input, so report the truncation
240            // here instead of panicking.
241            let decoded = if remaining.len() < 16 && remaining[0] == 0x18 {
242                Err(SBPFError::BytecodeError {
243                    error: format!("lddw needs 16 bytes but only {} remain", remaining.len()),
244                    span: 0..8, // Spans are relative to `remaining` and rebased by `pos` below, in the Err(e) arm.
245                    custom_label: None,
246                })
247            } else if is_sbpf_v2 {
248                // ugly v2 shit we need to fix goes here:
249                Instruction::from_bytes_sbpf_v2(remaining)
250            } else if is_sbpf_v3 {
251                Instruction::from_bytes_sbpf_v3(remaining)
252            } else {
253                Instruction::from_bytes(remaining)
254            };
255
256            let mut ix = match decoded {
257                Ok(ix) => ix,
258                // A word that fails to decode doesn't affect the rest of the
259                // stream, instead we record the error and keep it inline in the stream,
260                // where it occupies a slot to keep jump/call targets truthful.
261                Err(e) => {
262                    // Decode spans are relative to the instruction slice;
263                    // rebase them to the instruction's offset within .text.
264                    let e = match e {
265                        SBPFError::BytecodeError { error, span, .. } => {
266                            DisassemblerError::BytecodeError {
267                                error,
268                                span: span.start + pos..span.end + pos,
269                            }
270                        }
271                    };
272                    errors.push(e.clone());
273                    idx_to_slot.push(slot);
274                    ixs.push(Either::Right(e));
275                    pos += 8;
276                    slot += 1;
277                    continue;
278                }
279            };
280
281            // Handle syscall relocation
282            if ix.opcode == Opcode::Call
283                && let Some(Either::Right(Number::Int(-1))) = ix.imm
284                && let Some(syscall_name) = syscall_map.get(&(pos as u64))
285            {
286                ix.imm = Some(Either::Left(syscall_name.clone()));
287            }
288
289            idx_to_slot.push(slot);
290
291            if ix.opcode == Opcode::Lddw {
292                pos += 16;
293                slot += 2;
294            } else {
295                pos += 8;
296                slot += 1;
297            }
298
299            ixs.push(Either::Left(ix));
300        }
301
302        let mut slot_to_idx = vec![0usize; slot];
303        for (idx, &slot) in idx_to_slot.iter().enumerate() {
304            slot_to_idx[slot] = idx;
305        }
306
307        let text_sh_addr = self
308            .section_headers
309            .iter()
310            .find(|h| {
311                self.section_header_entries
312                    .iter()
313                    .any(|e| e.label.eq(".text\0") && e.offset == h.sh_offset as usize)
314            })
315            .map(|h| h.sh_addr)
316            .unwrap_or(0);
317        let text_end_addr = text_sh_addr + text_section.data.len() as u64;
318
319        let mut rodata_refs = BTreeSet::new();
320
321        if resolve_offsets {
322            // Resolve jump/call labels and collect rodata references
323
324            for (idx, ix) in ixs.iter_mut().enumerate() {
325                let Either::Left(ix) = ix else { continue };
326                let is_lddw = ix.opcode == Opcode::Lddw;
327
328                // Resolve jump targets
329                if ix.is_jump()
330                    && let Some(Either::Right(off)) = &ix.off
331                {
332                    let current_slot = idx_to_slot[idx] as i64;
333                    let target_slot = current_slot + 1 + (*off as i64);
334                    if target_slot >= 0
335                        && let Some(&target_idx) = slot_to_idx.get(target_slot as usize)
336                    {
337                        let new_off = target_idx as i64 - (idx as i64 + 1);
338                        ix.off = Some(Either::Right(new_off as i16));
339                    }
340                }
341
342                // Resolve internal call targets
343                if ix.opcode == Opcode::Call
344                    && let Some(Either::Right(Number::Int(imm))) = &ix.imm
345                {
346                    let current_slot = idx_to_slot[idx] as i64;
347                    let target_slot = current_slot + 1 + *imm;
348                    if target_slot >= 0
349                        && let Some(&target_idx) = slot_to_idx.get(target_slot as usize)
350                    {
351                        let new_rel = target_idx as i64 - (idx as i64 + 1);
352                        ix.imm = Some(Either::Right(Number::Int(new_rel)));
353                    }
354                }
355
356                // Collect rodata references
357                if is_lddw && let Some(Either::Right(Number::Int(imm))) = &ix.imm {
358                    let addr = *imm as u64;
359                    if rodata_info.is_some() && addr >= rodata_base && addr < rodata_end {
360                        rodata_refs.insert(addr);
361                    } else if addr >= text_sh_addr && addr < text_end_addr {
362                        // Convert text address to instruction index for callx.
363                        let byte_offset = addr - text_sh_addr;
364                        let target_slot = (byte_offset / 8) as usize;
365                        if target_slot < slot_to_idx.len() {
366                            let ix_idx = slot_to_idx[target_slot];
367                            ix.imm = Some(Either::Right(Number::Int(ix_idx as i64)));
368                        }
369                    }
370                }
371            }
372        }
373
374        // Parse rodata section
375        let rodata = if let Some((data, base_addr)) = rodata_info {
376            let mut section = RodataSection::parse(data, base_addr, &rodata_refs);
377            let (data_relocs, text_relocs) = self.classify_relocations(
378                &section.data,
379                base_addr,
380                text_section_offset,
381                text_section.data.len() as u64,
382                text_sh_addr,
383                &slot_to_idx,
384            );
385            section.data_relocations = data_relocs;
386            section.text_relocations = text_relocs;
387            Some(section)
388        } else {
389            None
390        };
391
392        // Calculate entrypoint instruction index from byte offset.
393        let entrypoint_idx = self.get_entrypoint_offset().map(|byte_offset| {
394            let entrypoint_slot = (byte_offset / 8) as usize;
395            if entrypoint_slot < slot_to_idx.len() {
396                slot_to_idx[entrypoint_slot]
397            } else {
398                0
399            }
400        });
401
402        Ok(Parsed {
403            value: Disassembly {
404                instructions: ixs,
405                rodata,
406                entrypoint: entrypoint_idx,
407            },
408            errors,
409        })
410    }
411
412    /// Build a hashmap where:
413    /// - key: relative position within .text section
414    /// - value: syscall name (sol_log_64_, sol_log_, etc.)
415    fn build_syscall_map(&self, text_section_offset: u64) -> HashMap<u64, String> {
416        self.relocations
417            .iter()
418            .filter(|r| r.is_syscall())
419            .filter_map(|r| {
420                r.symbol_name.as_ref().map(|name| {
421                    // Convert absolute offset to relative position within .text
422                    let relative_pos = r.relative_offset(text_section_offset);
423                    (relative_pos, name.clone())
424                })
425            })
426            .collect()
427    }
428
429    /// Get the raw rodata bytes and the virtual address where it's loaded in memory
430    fn get_rodata_info(&self) -> Option<(Vec<u8>, u64)> {
431        let rodata_entry = self
432            .section_header_entries
433            .iter()
434            .find(|e| e.label.starts_with(".rodata"))?;
435
436        // v3: use program header p_vaddr
437        // v0: use section header sh_addr
438        let vaddr = if self.is_v3() {
439            self.program_headers
440                .iter()
441                .find(|ph| {
442                    let rodata_offset = rodata_entry.offset as u64;
443                    rodata_offset >= ph.p_offset && rodata_offset < ph.p_offset + ph.p_filesz
444                })
445                .map(|ph| ph.p_vaddr)
446                .unwrap_or(0)
447        } else {
448            let rodata_header = self
449                .section_headers
450                .iter()
451                .find(|h| h.sh_offset as usize == rodata_entry.offset)?;
452            rodata_header.sh_addr
453        };
454
455        // Check for .data.rel.ro section and combine if present.
456        if let Some(data_rel_ro_entry) = self
457            .section_header_entries
458            .iter()
459            .find(|e| e.label.starts_with(".data.rel.ro"))
460        {
461            let data_rel_ro_header = self
462                .section_headers
463                .iter()
464                .find(|h| h.sh_offset as usize == data_rel_ro_entry.offset);
465
466            if let Some(drr_header) = data_rel_ro_header {
467                let drr_end = drr_header.sh_addr + drr_header.sh_size;
468                let total_size = (drr_end - vaddr) as usize;
469
470                // Allocate combined buffer.
471                let mut combined = vec![0u8; total_size];
472
473                // Copy .rodata at offset 0.
474                let rodata_len = rodata_entry.data.len().min(total_size);
475                combined[..rodata_len].copy_from_slice(&rodata_entry.data[..rodata_len]);
476
477                // Copy .data.rel.ro at its offset relative to rodata base.
478                let drr_offset = (drr_header.sh_addr - vaddr) as usize;
479                let drr_len = data_rel_ro_entry.data.len().min(total_size - drr_offset);
480                combined[drr_offset..drr_offset + drr_len]
481                    .copy_from_slice(&data_rel_ro_entry.data[..drr_len]);
482
483                return Some((combined, vaddr));
484            }
485        }
486
487        Some((rodata_entry.data.clone(), vaddr))
488    }
489
490    /// Classify relocations into data and text relocations.
491    fn classify_relocations(
492        &self,
493        rodata_data: &[u8],
494        rodata_base: u64,
495        text_offset: u64,
496        text_len: u64,
497        text_sh_addr: u64,
498        slot_to_idx: &[usize],
499    ) -> (Vec<usize>, Vec<(usize, usize)>) {
500        let rodata_len = rodata_data.len();
501        let text_end_addr = text_sh_addr + text_len;
502        let mut data_relocs = Vec::new();
503        let mut text_relocs = Vec::new();
504
505        for r in &self.relocations {
506            if r.rel_type != crate::relocation::RelocationType::R_BPF_64_RELATIVE {
507                continue;
508            }
509            if r.offset >= text_offset && r.offset < text_offset + text_len {
510                continue;
511            }
512            if r.offset < rodata_base || r.offset + 8 > rodata_base + rodata_len as u64 {
513                continue;
514            }
515            let offset_in_blob = (r.offset - rodata_base) as usize;
516            data_relocs.push(offset_in_blob);
517
518            let imm_offset = offset_in_blob + 4;
519            if imm_offset + 4 <= rodata_len {
520                let ptr =
521                    u32::from_le_bytes(rodata_data[imm_offset..imm_offset + 4].try_into().unwrap())
522                        as u64;
523                if ptr >= text_sh_addr && ptr < text_end_addr {
524                    let target_slot = ((ptr - text_sh_addr) / 8) as usize;
525                    if target_slot < slot_to_idx.len() {
526                        text_relocs.push((offset_in_blob, slot_to_idx[target_slot]));
527                    }
528                }
529            }
530        }
531
532        (data_relocs, text_relocs)
533    }
534
535    /// Get the entrypoint offset
536    pub fn get_entrypoint_offset(&self) -> Option<u64> {
537        let e_entry = self.elf_header.e_entry;
538
539        if self.is_v3() {
540            const V3_BYTECODE_VADDR: u64 = 1 << 32;
541            if e_entry >= V3_BYTECODE_VADDR {
542                Some(e_entry - V3_BYTECODE_VADDR)
543            } else {
544                None
545            }
546        } else {
547            let text_header = self.section_headers.iter().find(|h| {
548                self.section_header_entries
549                    .iter()
550                    .any(|e| e.label.eq(".text\0") && e.offset == h.sh_offset as usize)
551            })?;
552            let text_sh_addr = text_header.sh_addr;
553
554            if e_entry >= text_sh_addr {
555                Some(e_entry - text_sh_addr)
556            } else {
557                None
558            }
559        }
560    }
561
562    fn is_v3(&self) -> bool {
563        self.elf_header.e_flags == 0x03 && self.elf_header.e_machine == E_MACHINE
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use {
570        crate::{
571            elf_header::{E_MACHINE, E_MACHINE_SBPF, EI_OSABI_LINUX, ELFHeader},
572            program::Program,
573            section_header_entry::SectionHeaderEntry,
574        },
575        hex_literal::hex,
576    };
577
578    #[test]
579    fn try_deserialize_program() {
580        let mut bytes = hex!("7F454C460201010000000000000000000300F700010000002001000000000000400000000000000028020000000000000000000040003800030040000600050001000000050000002001000000000000200100000000000020010000000000003000000000000000300000000000000000100000000000000100000004000000C001000000000000C001000000000000C0010000000000003C000000000000003C000000000000000010000000000000020000000600000050010000000000005001000000000000500100000000000070000000000000007000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007912A000000000007911182900000000B7000000010000002D21010000000000B70000000000000095000000000000001E0000000000000004000000000000000600000000000000C0010000000000000B0000000000000018000000000000000500000000000000F0010000000000000A000000000000000C00000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000120001002001000000000000300000000000000000656E747279706F696E7400002E74657874002E64796E737472002E64796E73796D002E64796E616D6963002E73687374727461620000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000010000000600000000000000200100000000000020010000000000003000000000000000000000000000000008000000000000000000000000000000170000000600000003000000000000005001000000000000500100000000000070000000000000000400000000000000080000000000000010000000000000000F0000000B0000000200000000000000C001000000000000C001000000000000300000000000000004000000010000000800000000000000180000000000000007000000030000000200000000000000F001000000000000F0010000000000000C00000000000000000000000000000001000000000000000000000000000000200000000300000000000000000000000000000000000000FC010000000000002A00000000000000000000000000000001000000000000000000000000000000").to_vec();
581        let program = Program::from_bytes(&bytes).unwrap();
582        println!("{:?}", program.section_header_entries);
583
584        bytes[7] = EI_OSABI_LINUX;
585        let program = Program::from_bytes(&bytes).unwrap();
586        assert_eq!(program.elf_header.ei_osabi, EI_OSABI_LINUX);
587
588        // Corrupt e_machine (LE u16 at bytes 18..20): parsing fails and the
589        // error carries the field name, the accepted values and the value
590        // found.
591        bytes[18] = 0x00;
592        let errors = Program::from_bytes(&bytes).unwrap_err();
593        match errors.as_slice() {
594            [
595                crate::errors::DisassemblerError::NonStandardElfHeader {
596                    field,
597                    expected,
598                    found,
599                },
600            ] => {
601                assert_eq!(*field, "machine");
602                assert_eq!(*expected, vec![E_MACHINE as u64, E_MACHINE_SBPF as u64]);
603                assert_eq!(*found, 0);
604            }
605            other => panic!("expected NonStandardElfHeader for machine, got {other:?}"),
606        }
607    }
608
609    #[test]
610    fn test_from_bytes_reports_all_header_errors() {
611        let mut bytes = hex!("7F454C460201010000000000000000000300F700010000002001000000000000400000000000000028020000000000000000000040003800030040000600050001000000050000002001000000000000200100000000000020010000000000003000000000000000300000000000000000100000000000000100000004000000C001000000000000C001000000000000C0010000000000003C000000000000003C000000000000000010000000000000020000000600000050010000000000005001000000000000500100000000000070000000000000007000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007912A000000000007911182900000000B7000000010000002D21010000000000B70000000000000095000000000000001E0000000000000004000000000000000600000000000000C0010000000000000B0000000000000018000000000000000500000000000000F0010000000000000A000000000000000C00000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000120001002001000000000000300000000000000000656E747279706F696E7400002E74657874002E64796E737472002E64796E73796D002E64796E616D6963002E73687374727461620000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000010000000600000000000000200100000000000020010000000000003000000000000000000000000000000008000000000000000000000000000000170000000600000003000000000000005001000000000000500100000000000070000000000000000400000000000000080000000000000010000000000000000F0000000B0000000200000000000000C001000000000000C001000000000000300000000000000004000000010000000800000000000000180000000000000007000000030000000200000000000000F001000000000000F0010000000000000C00000000000000000000000000000001000000000000000000000000000000200000000300000000000000000000000000000000000000FC010000000000002A00000000000000000000000000000001000000000000000000000000000000").to_vec();
612
613        // Corrupt two independent header fields: os abi (byte 7) and
614        // e_version (LE u32 at bytes 20..24). One Err reports both, not
615        // just the first.
616        bytes[7] = 0x05;
617        bytes[20] = 0x02;
618
619        let errors = Program::from_bytes(&bytes).unwrap_err();
620        let fields: Vec<&str> = errors
621            .iter()
622            .map(|e| match e {
623                crate::errors::DisassemblerError::NonStandardElfHeader { field, .. } => *field,
624                other => panic!("expected NonStandardElfHeader, got {other:?}"),
625            })
626            .collect();
627        assert_eq!(fields, vec!["os abi", "version"]);
628    }
629
630    #[test]
631    fn test_to_ixs_invalid_data_length() {
632        // Create program with .text section that has invalid length (not multiple of 8)
633        let program = Program {
634            elf_header: ELFHeader {
635                ei_magic: [127, 69, 76, 70],
636                ei_class: 2,
637                ei_data: 1,
638                ei_version: 1,
639                ei_osabi: 0,
640                ei_abiversion: 0,
641                ei_pad: [0; 7],
642                e_type: 0,
643                e_machine: 0,
644                e_version: 0,
645                e_entry: 0,
646                e_phoff: 0,
647                e_shoff: 0,
648                e_flags: 0,
649                e_ehsize: 0,
650                e_phentsize: 0,
651                e_phnum: 0,
652                e_shentsize: 0,
653                e_shnum: 0,
654                e_shstrndx: 0,
655            },
656            program_headers: vec![],
657            section_headers: vec![],
658            section_header_entries: vec![
659                SectionHeaderEntry::new(".text\0".to_string(), 0, vec![0x95, 0x00, 0x00]).unwrap(), // Only 3 bytes
660            ],
661            relocations: vec![],
662        };
663
664        let parsed = program.to_ixs().unwrap();
665        assert!(parsed.value.instructions.is_empty());
666        assert!(matches!(
667            parsed.errors.as_slice(),
668            [crate::errors::DisassemblerError::InvalidDataLength(3)]
669        ));
670    }
671
672    #[test]
673    fn test_to_ixs_with_lddw() {
674        // Test with 16 bytes lddw instruction
675
676        let mut lddw_bytes = vec![0x18, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
677        lddw_bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
678        lddw_bytes.extend_from_slice(&[0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // exit
679
680        let program = Program {
681            elf_header: ELFHeader {
682                ei_magic: [127, 69, 76, 70],
683                ei_class: 2,
684                ei_data: 1,
685                ei_version: 1,
686                ei_osabi: 0,
687                ei_abiversion: 0,
688                ei_pad: [0; 7],
689                e_type: 0,
690                e_machine: E_MACHINE_SBPF,
691                e_version: 0,
692                e_entry: 0,
693                e_phoff: 0,
694                e_shoff: 0,
695                e_flags: 0,
696                e_ehsize: 0,
697                e_phentsize: 0,
698                e_phnum: 0,
699                e_shentsize: 0,
700                e_shnum: 0,
701                e_shstrndx: 0,
702            },
703            program_headers: vec![],
704            section_headers: vec![],
705            section_header_entries: vec![
706                SectionHeaderEntry::new(".text\0".to_string(), 0, lddw_bytes).unwrap(),
707            ],
708            relocations: vec![],
709        };
710
711        let parsed = program.to_ixs().unwrap();
712        assert!(parsed.errors.is_empty());
713        let ixs = parsed.value.instructions;
714        assert_eq!(ixs.len(), 2); // lddw + exit
715        assert_eq!(
716            ixs[0].as_ref().unwrap_left().opcode,
717            sbpf_common::opcode::Opcode::Lddw
718        );
719    }
720
721    #[test]
722    fn test_to_ixs_sbpf_v2() {
723        // Use a v2 opcode (0x8C -> ldxw in v2)
724        let v2_bytes = vec![0x8c, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
725
726        let program = Program {
727            elf_header: ELFHeader {
728                ei_magic: [127, 69, 76, 70],
729                ei_class: 2,
730                ei_data: 1,
731                ei_version: 1,
732                ei_osabi: 0,
733                ei_abiversion: 0,
734                ei_pad: [0; 7],
735                e_type: 0,
736                e_machine: E_MACHINE_SBPF,
737                e_version: 0,
738                e_entry: 0,
739                e_phoff: 0,
740                e_shoff: 0,
741                e_flags: 0x02, // SBPF v2 flag
742                e_ehsize: 0,
743                e_phentsize: 0,
744                e_phnum: 0,
745                e_shentsize: 0,
746                e_shnum: 0,
747                e_shstrndx: 0,
748            },
749            program_headers: vec![],
750            section_headers: vec![],
751            section_header_entries: vec![
752                SectionHeaderEntry::new(".text\0".to_string(), 0, v2_bytes).unwrap(),
753            ],
754            relocations: vec![],
755        };
756
757        let parsed = program.to_ixs().unwrap();
758        assert!(parsed.errors.is_empty());
759        let ixs = parsed.value.instructions;
760        assert_eq!(ixs.len(), 1);
761        assert_eq!(
762            ixs[0].as_ref().unwrap_left().opcode,
763            sbpf_common::opcode::Opcode::Ldxw
764        );
765    }
766
767    #[test]
768    fn test_to_ixs_sbpf_v3() {
769        let v3_bytes = vec![0x46, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00];
770
771        let program = Program {
772            elf_header: ELFHeader {
773                ei_magic: [127, 69, 76, 70],
774                ei_class: 2,
775                ei_data: 1,
776                ei_version: 1,
777                ei_osabi: 0,
778                ei_abiversion: 0,
779                ei_pad: [0; 7],
780                e_type: 0,
781                e_machine: E_MACHINE,
782                e_version: 0,
783                e_entry: 0,
784                e_phoff: 0,
785                e_shoff: 0,
786                e_flags: 0x03, // SBPF v3 flag
787                e_ehsize: 0,
788                e_phentsize: 0,
789                e_phnum: 0,
790                e_shentsize: 0,
791                e_shnum: 0,
792                e_shstrndx: 0,
793            },
794            program_headers: vec![],
795            section_headers: vec![],
796            section_header_entries: vec![
797                SectionHeaderEntry::new(".text\0".to_string(), 0, v3_bytes).unwrap(),
798            ],
799            relocations: vec![],
800        };
801
802        let parsed = program.to_ixs().unwrap();
803        assert!(parsed.errors.is_empty());
804        let ixs = parsed.value.instructions;
805        assert_eq!(ixs.len(), 1);
806        assert_eq!(
807            ixs[0].as_ref().unwrap_left().opcode,
808            sbpf_common::opcode::Opcode::Jset32Imm
809        );
810    }
811
812    #[test]
813    fn test_to_ixs_skips_undecodable_instruction() {
814        // .text: [8 bytes of garbage][exit]. The garbage word is reported
815        // and kept inline in the instruction stream; decoding resumes at
816        // the next 8-byte boundary.
817        let mut text = vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
818        text.extend_from_slice(&[0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
819
820        let program = Program {
821            elf_header: ELFHeader {
822                ei_magic: [127, 69, 76, 70],
823                ei_class: 2,
824                ei_data: 1,
825                ei_version: 1,
826                ei_osabi: 0,
827                ei_abiversion: 0,
828                ei_pad: [0; 7],
829                e_type: 0,
830                e_machine: E_MACHINE_SBPF,
831                e_version: 0,
832                e_entry: 0,
833                e_phoff: 0,
834                e_shoff: 0,
835                e_flags: 0,
836                e_ehsize: 0,
837                e_phentsize: 0,
838                e_phnum: 0,
839                e_shentsize: 0,
840                e_shnum: 0,
841                e_shstrndx: 0,
842            },
843            program_headers: vec![],
844            section_headers: vec![],
845            section_header_entries: vec![
846                SectionHeaderEntry::new(".text\0".to_string(), 0, text).unwrap(),
847            ],
848            relocations: vec![],
849        };
850
851        let parsed = program.to_ixs().unwrap();
852        assert_eq!(parsed.value.instructions.len(), 2);
853        match &parsed.value.instructions[0] {
854            either::Either::Right(crate::errors::DisassemblerError::BytecodeError {
855                span, ..
856            }) => assert_eq!(span.start, 0),
857            other => panic!("expected inline BytecodeError, got {other:?}"),
858        }
859        assert_eq!(
860            parsed.value.instructions[1].as_ref().unwrap_left().opcode,
861            sbpf_common::opcode::Opcode::Exit
862        );
863        match parsed.errors.as_slice() {
864            [crate::errors::DisassemblerError::BytecodeError { span, .. }] => {
865                assert_eq!(span.start, 0);
866            }
867            other => panic!("expected one BytecodeError, got {other:?}"),
868        }
869    }
870
871    #[test]
872    fn test_to_ixs_reports_truncated_lddw() {
873        // .text: [exit][lddw first half with no second half]. The truncated
874        // lddw is reported and skipped instead of panicking in the decoder.
875        let mut text = vec![0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
876        text.extend_from_slice(&[0x18, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]);
877
878        let program = Program {
879            elf_header: ELFHeader {
880                ei_magic: [127, 69, 76, 70],
881                ei_class: 2,
882                ei_data: 1,
883                ei_version: 1,
884                ei_osabi: 0,
885                ei_abiversion: 0,
886                ei_pad: [0; 7],
887                e_type: 0,
888                e_machine: E_MACHINE_SBPF,
889                e_version: 0,
890                e_entry: 0,
891                e_phoff: 0,
892                e_shoff: 0,
893                e_flags: 0,
894                e_ehsize: 0,
895                e_phentsize: 0,
896                e_phnum: 0,
897                e_shentsize: 0,
898                e_shnum: 0,
899                e_shstrndx: 0,
900            },
901            program_headers: vec![],
902            section_headers: vec![],
903            section_header_entries: vec![
904                SectionHeaderEntry::new(".text\0".to_string(), 0, text).unwrap(),
905            ],
906            relocations: vec![],
907        };
908
909        let parsed = program.to_ixs().unwrap();
910        assert_eq!(parsed.value.instructions.len(), 2);
911        assert_eq!(
912            parsed.value.instructions[0].as_ref().unwrap_left().opcode,
913            sbpf_common::opcode::Opcode::Exit
914        );
915        assert!(parsed.value.instructions[1].is_right());
916        match parsed.errors.as_slice() {
917            [crate::errors::DisassemblerError::BytecodeError { error, span }] => {
918                assert_eq!(*span, (8..16));
919                assert_eq!(error, "lddw needs 16 bytes but only 8 remain");
920            }
921            other => panic!("expected one BytecodeError, got {other:?}"),
922        }
923    }
924
925    #[test]
926    fn test_to_ixs_decodes_words_before_trailing_bytes() {
927        // .text: exit + 3 trailing bytes (11 total). The length is reported
928        // but the full 8-byte word still decodes.
929        let text = vec![
930            0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
931        ];
932
933        let program = Program {
934            elf_header: ELFHeader {
935                ei_magic: [127, 69, 76, 70],
936                ei_class: 2,
937                ei_data: 1,
938                ei_version: 1,
939                ei_osabi: 0,
940                ei_abiversion: 0,
941                ei_pad: [0; 7],
942                e_type: 0,
943                e_machine: E_MACHINE_SBPF,
944                e_version: 0,
945                e_entry: 0,
946                e_phoff: 0,
947                e_shoff: 0,
948                e_flags: 0,
949                e_ehsize: 0,
950                e_phentsize: 0,
951                e_phnum: 0,
952                e_shentsize: 0,
953                e_shnum: 0,
954                e_shstrndx: 0,
955            },
956            program_headers: vec![],
957            section_headers: vec![],
958            section_header_entries: vec![
959                SectionHeaderEntry::new(".text\0".to_string(), 0, text).unwrap(),
960            ],
961            relocations: vec![],
962        };
963
964        let parsed = program.to_ixs().unwrap();
965        assert_eq!(parsed.value.instructions.len(), 1);
966        assert!(matches!(
967            parsed.errors.as_slice(),
968            [crate::errors::DisassemblerError::InvalidDataLength(11)]
969        ));
970    }
971}