Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source-level-debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Soroban contracts are compiled from Rust to WebAssembly (WASM). While debugging
- **DWARF Support**: Full support for standard DWARF embedded in WASM.
- **Source Line Stepping**: Integrated into the stepping logic.
- **Caching**: Performance optimized with file and mapping caches.
- **Fallback**: Graceful fallback to WASM-only view if debug info is missing or stripped.
- **Fallback & Diagnostics**: Graceful fallback to WASM-only view if debug info is missing or stripped. When DWARF metadata is partially malformed, `SourceMap::load` continues to extract valid data and surfaces parsing errors as warnings (`SourceMapDiagnostic`) rather than completely aborting. These diagnostics can be reviewed using `inspect`.

## Limitations

Expand Down
59 changes: 54 additions & 5 deletions src/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ struct FunctionListing {
exported_functions: Vec<FunctionSignatureJson>,
}

#[derive(Serialize)]
struct SourceMapReport {
mappings_count: usize,
diagnostics: Vec<crate::debugger::source_map::SourceMapDiagnostic>,
}

#[derive(Serialize)]
struct FullReport {
file: String,
Expand All @@ -64,6 +70,7 @@ struct FullReport {
functions: Vec<String>,
signatures: Vec<crate::utils::wasm::FunctionSignature>,
metadata: crate::utils::wasm::ContractMetadata,
source_map: SourceMapReport,
}

fn output_functions(path: &Path, wasm_bytes: &[u8], format: OutputFormat) -> Result<()> {
Expand Down Expand Up @@ -127,13 +134,25 @@ fn print_pretty_functions(signatures: &[crate::utils::wasm::FunctionSignature],
}

fn print_json_report(path: &Path, wasm_bytes: &[u8]) -> Result<()> {
let info = get_module_info(wasm_bytes)?;
let functions = parse_functions(wasm_bytes)?;
let signatures = parse_function_signatures(wasm_bytes)?;
let metadata = extract_contract_metadata(wasm_bytes)?;

let mut source_map = crate::debugger::source_map::SourceMap::new();
let _ = source_map.load(wasm_bytes);

let report = FullReport {
file: path.display().to_string(),
size_bytes: wasm_bytes.len(),
module_info: get_module_info(wasm_bytes)?,
functions: parse_functions(wasm_bytes)?,
signatures: parse_function_signatures(wasm_bytes)?,
metadata: extract_contract_metadata(wasm_bytes)?,
module_info: info,
functions,
signatures,
metadata,
source_map: SourceMapReport {
mappings_count: source_map.len(),
diagnostics: source_map.diagnostics.clone(),
},
};

crate::logging::log_display(
Expand Down Expand Up @@ -229,7 +248,37 @@ fn print_report(path: &Path, wasm_bytes: &[u8]) -> Result<()> {
}
});

log_both(&separator);
section_header("Contract Metadata");
if metadata.is_empty() {
log_both(" ⚠ No metadata section embedded in this contract.");
} else {
log_both_if_some("Contract Version", &metadata.contract_version);
log_both_if_some("SDK Version", &metadata.sdk_version);
log_both_if_some("Build Date", &metadata.build_date);
log_both_if_some("Author / Org", &metadata.author);
log_both_if_some("Description", &metadata.description);
log_both_if_some("Implementation", &metadata.implementation);
}
log_both("");

section_header("Source Map (DWARF)");
let mut source_map = crate::debugger::source_map::SourceMap::new();
let _ = source_map.load(wasm_bytes);

if source_map.is_empty() && source_map.diagnostics.is_empty() {
log_both(" No DWARF debug information found in this contract.");
} else {
log_both(&format!(" Mapped Executable Lines : {}", source_map.len().to_string().bright_white()));
if !source_map.diagnostics.is_empty() {
log_both("");
log_both(&format!(" {}", "⚠ Diagnostics / Warnings:".yellow().bold()));
for diag in &source_map.diagnostics {
log_both(&format!(" - {}", diag.message.yellow()));
}
}
}

log_both(&heavy);
Ok(())
}

Expand Down
69 changes: 57 additions & 12 deletions src/debugger/source_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ pub struct SourceLocation {
pub column: Option<u32>,
}

/// A diagnostic message indicating an issue with loading DWARF debug metadata.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SourceMapDiagnostic {
pub message: String,
}

/// Manages mapping from WASM offsets to source code locations
pub struct SourceMap {
/// Mapping from offset to source location (sorted by offset)
Expand All @@ -21,6 +27,8 @@ pub struct SourceMap {
source_cache: HashMap<PathBuf, String>,
/// Code section payload range (when known), used to normalize DWARF addresses.
code_section_range: Option<std::ops::Range<usize>>,
/// Diagnostics accumulated during DWARF parsing
pub diagnostics: Vec<SourceMapDiagnostic>,
}

/// Result of resolving a source breakpoint (file + line) to a concrete contract entrypoint breakpoint.
Expand Down Expand Up @@ -57,6 +65,7 @@ impl SourceMap {
offsets: BTreeMap::new(),
source_cache: HashMap::new(),
code_section_range: None,
diagnostics: Vec::new(),
}
}

Expand Down Expand Up @@ -87,22 +96,54 @@ impl SourceMap {
Ok(EndianSlice::new(data, RunTimeEndian::Little))
};

let dwarf = Dwarf::load(&load_section).map_err(|e| {
DebuggerError::WasmLoadError(format!("Failed to load DWARF sections: {}", e))
})?;
let dwarf = match Dwarf::load(&load_section) {
Ok(d) => d,
Err(e) => {
self.diagnostics.push(SourceMapDiagnostic {
message: format!("Failed to load DWARF sections: {}", e),
});
// We cannot proceed without the main DWARF sections headers successfully parsed
return Err(DebuggerError::WasmLoadError(format!("DWARF sections severely malformed: {}", e)).into());
}
};

let mut units = dwarf.units();
while let Some(header) = units.next().map_err(|e| {
DebuggerError::WasmLoadError(format!("Failed to read DWARF unit: {}", e))
})? {
let unit = dwarf.unit(header).map_err(|e| {
DebuggerError::WasmLoadError(format!("Failed to load DWARF unit: {}", e))
})?;
loop {
let header = match units.next() {
Ok(Some(h)) => h,
Ok(None) => break,
Err(e) => {
self.diagnostics.push(SourceMapDiagnostic {
message: format!("Failed to read DWARF unit header: {}", e),
});
break;
}
};

let unit = match dwarf.unit(header) {
Ok(u) => u,
Err(e) => {
self.diagnostics.push(SourceMapDiagnostic {
message: format!("Failed to load DWARF unit content: {}", e),
});
continue; // try next unit
}
};

if let Some(program) = unit.line_program.clone() {
let mut rows = program.rows();
while let Some((header, row)) = rows.next_row().map_err(|e| {
DebuggerError::WasmLoadError(format!("Failed to read DWARF line row: {}", e))
})? {
loop {
let (header, row) = match rows.next_row() {
Ok(Some(t)) => t,
Ok(None) => break,
Err(e) => {
self.diagnostics.push(SourceMapDiagnostic {
message: format!("Failed to read DWARF line row: {}", e),
});
break; // break row iteration for this unit, continue to next unit
}
};

if let Some(file_path) =
self.get_file_path(&dwarf, &unit, header, row.file_index())
{
Expand All @@ -125,6 +166,10 @@ impl SourceMap {
);
}
}
} else {
self.diagnostics.push(SourceMapDiagnostic {
message: format!("DWARF unit is missing a line program (e.g., .debug_line section data missing or malformed)."),
});
}
}

Expand Down
48 changes: 48 additions & 0 deletions tests/source_map_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,51 @@ fn source_map_debug_fixture_resolves_locations() {
// Range lookup should return the same location for nearby offsets.
assert!(sm.lookup(first_offset.saturating_add(1)).is_some());
}

fn uleb128(mut v: usize) -> Vec<u8> {
let mut out = Vec::new();
loop {
let mut b = (v & 0x7F) as u8;
v >>= 7;
if v != 0 {
b |= 0x80;
}
out.push(b);
if v == 0 {
break;
}
}
out
}

fn wasm_with_custom_section(name: &str, payload: &[u8]) -> Vec<u8> {
let mut bytes: Vec<u8> = Vec::new();
bytes.extend_from_slice(&[0x00, 0x61, 0x73, 0x6d]);
bytes.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]);
bytes.push(0x00); // custom section id

let mut section = Vec::new();
section.extend_from_slice(&uleb128(name.len()));
section.extend_from_slice(name.as_bytes());
section.extend_from_slice(payload);

bytes.extend_from_slice(&uleb128(section.len()));
bytes.extend_from_slice(&section);
bytes
}

#[test]
fn source_map_partial_dwarf_is_graceful() {
// A WASM with a completely malformed debug_info section.
let malicious_dwarf = wasm_with_custom_section(".debug_info", &[0xde, 0xad, 0xbe, 0xef]);
let mut sm = SourceMap::new();
let res = sm.load(&malicious_dwarf);

// The load should succeed but produce no mappings and one or more diagnostics.
assert!(res.is_ok(), "load should not fail on partial/malformed DWARF units");
assert!(sm.is_empty(), "expected no mappings for garbage DWARF");
assert!(!sm.diagnostics.is_empty(), "expected diagnostics explaining the failure");

let diag = &sm.diagnostics[0];
assert!(diag.message.contains("Failed to read"), "Diagnostics should mention read failure: {}", diag.message);
}
Loading