From c7d92071e1dd11c45999457b16554b4abaa73b28 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 2 Aug 2026 15:21:34 +0200 Subject: v0.5.5: crash reports go to a dedicated CRASHLOG USB stick The fault handler now writes crash.log to the volume labeled CRASHLOG (label-selected via fat::crash_vol), falling back to slot 0. On real hardware the boot USB may be unwritable, so a second stick labeled CRASHLOG survives the write; the shell auto-prints the previous crash report from it at next boot (read_crash_log resolves /disk//crash.log through the FAT path, since Disk nodes hold no cached children). - fat.rs: volume_label() + crash_vol() (no allocation) - fs.rs: write_crash_log targets crash_vol(); read_crash_log prefers the CRASHLOG disk node; drop now-unused first_disk() - mkdisk.sh: --label option - test-fault.sh: boots with a dedicated CRASHLOG stick (mkfs -F 32 -n CRASHLOG), verifies crash.log lands there and NOT on the ESP --- rsnix/src/fs.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 2 deletions(-) (limited to 'rsnix/src/fs.rs') diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs index 5fc6d7a..24d8fcd 100644 --- a/rsnix/src/fs.rs +++ b/rsnix/src/fs.rs @@ -464,9 +464,98 @@ pub fn write(h: usize, buf: &[u8], pos: u64) -> Result { } } +/// Handle of `crash.log` on the crash-log volume, if it exists. The +/// crash-log volume is the one labeled CRASHLOG (a dedicated stick that +/// survives reboot writes), else the first mounted volume. Resolves the +/// FAT path (`/disk//crash.log`) because Disk nodes hold no cached +/// children. +pub fn read_crash_log() -> Option { + let disk_dir = lookup(0, "disk")?; + let children = match &nodes()[disk_dir].data { + NodeData::Dir(v) => v.clone(), + _ => return None, + }; + // Prefer a disk node whose name starts with "CRASHLOG-". + let mut first = None; + for &c in &children { + if nodes()[c].removed || !matches!(nodes()[c].data, NodeData::Disk(_)) { + continue; + } + let name = &nodes()[c].name; + let label = name.split_once('-').map(|(l, _)| l).unwrap_or(name); + if label.eq_ignore_ascii_case("CRASHLOG") { + return resolve(&alloc::format!("/disk/{}/crash.log", name)); + } + if first.is_none() { + first = Some(c); + } + } + first.and_then(|disk| { + let name = nodes()[disk].name.clone(); + resolve(&alloc::format!("/disk/{}/crash.log", name)) + }) +} + +/// Persist a crash report to the mounted FAT volume so it survives a reboot +/// (real hardware has no serial console). Best-effort: any failure is +/// ignored — the caller (fault handler) must never fault here; the +/// in-dump ISR guard catches that case and halts. +/// +/// Goes straight at the FAT layer (the crash-log volume + its root +/// cluster) rather than through the fs node table: the fault may have +/// interrupted the fs mid-operation, and a stale DiskInfo/ACTIVE_VOL +/// would make node-based writes fail with "no volume at slot N". The +/// crash-log volume is the one labeled CRASHLOG (a dedicated stick that +/// survives reboot writes), else slot 0 (first mounted) as before. +pub fn write_crash_log(lines: &[&str]) { + // Assemble the report on the stack (heap-free — a corrupted heap is + // exactly the situation that produces crash reports). + let mut text = [0u8; 640]; + let mut n = 0usize; + for line in lines { + for &b in line.as_bytes() { + if n < text.len() { + text[n] = b; + n += 1; + } + } + if n < text.len() { + text[n] = b'\n'; + n += 1; + } + } + let Some(vol) = crate::fat::crash_vol() else { + crate::serial::write_raw("crashlog: no volume\r\n"); + return; + }; + // Trace the target so a test can verify the right stick got the log. + let label = crate::fat::volume_label(vol).unwrap_or("?"); + let mut tb = [0u8; 48]; + let tn = { + let mut w = crate::strbuf::FmtBuf::new(&mut tb); + let _ = core::fmt::write(&mut w, core::format_args!("crashlog: vol {} ({})\r\n", vol, label)); + w.len() + }; + crate::serial::write_raw(core::str::from_utf8(&tb[..tn]).unwrap_or("?")); + let root = crate::fat::root_cluster(vol); + if crate::fat::create(vol, root, "crash.log", false).is_err() + && crate::fat::find(vol, root, "crash.log").is_err() + { + crate::serial::write_raw("crashlog: create failed\r\n"); + return; + } + if crate::fat::set_len(vol, root, "crash.log", 0).is_err() { + crate::serial::write_raw("crashlog: set_len failed\r\n"); + return; + } + match crate::fat::write_file(vol, root, "crash.log", &text[..n], 0) { + Ok(_) => crate::serial::write_raw("crashlog: wrote\r\n"), + Err(_) => crate::serial::write_raw("crashlog: write failed\r\n"), + } +} + /// Truncate/resize a regular file. -pub fn set_len(h: usize, len: u64) -> Result<(), ()> { - match &mut nodes()[h].data { +pub fn set_len(h: usize, len: u64) -> Result<(), ()> { match &mut nodes()[h].data { NodeData::Regular(v) => { v.resize(len as usize, 0); Ok(()) -- cgit v1.2.3