mycelium_kernel/arch/x86_64/
oops.rs

1use super::framebuf;
2use core::{
3    panic::PanicInfo,
4    sync::atomic::{AtomicBool, Ordering},
5};
6use embedded_graphics::{
7    mono_font::{ascii, MonoTextStyleBuilder},
8    pixelcolor::{Rgb888, RgbColor as _},
9    prelude::*,
10    text::{Alignment, Text},
11};
12use hal_core::{
13    framebuffer::{Draw, RgbColor},
14    interrupt, Address,
15};
16use hal_x86_64::{control_regs, cpu, interrupt::Registers as X64Registers, serial, vga};
17use mycelium_trace::{embedded_graphics::TextWriterBuilder, writer::MakeWriter};
18use mycelium_util::fmt::{self, Write};
19
20#[derive(Debug)]
21pub struct Oops<'a> {
22    already_panicked: bool,
23    already_faulted: bool,
24    alloc: crate::allocator::State,
25    situation: OopsSituation<'a>,
26}
27
28enum OopsSituation<'a> {
29    Fault {
30        kind: &'static str,
31        fault: Fault<'a>,
32        details: Option<&'a dyn fmt::Display>,
33    },
34    Panic(&'a PanicInfo<'a>),
35    AllocError(alloc::alloc::Layout),
36}
37
38type Fault<'a> = &'a dyn interrupt::ctx::Context<Registers = X64Registers>;
39
40#[cold]
41pub fn oops(oops: Oops<'_>) -> ! {
42    let mut vga = vga::writer();
43
44    // /!\ disable all interrupts, unlock everything to prevent deadlock /!\
45    //
46    // Safety: it is okay to do this because we are oopsing and everything
47    // is going to die anyway.
48    unsafe {
49        // disable all interrupts.
50        cpu::intrinsics::cli();
51
52        // If the system has a COM1, unlock it.
53        if let Some(com1) = serial::com1() {
54            com1.force_unlock();
55        }
56
57        // unlock the VGA buffer.
58        vga.force_unlock();
59
60        // unlock the frame buffer
61        framebuf::force_unlock();
62    }
63
64    // emit a DEBUG event first. with the default tracing configuration, these
65    // will go to the serial port and *not* get written to the framebuffer. this
66    // is important, since it lets us still get information about the oops on
67    // the serial port, even if the oops was due to a bug in eliza's framebuffer
68    // code, which (it turns out) is surprisingly janky...
69    tracing::debug!(target: "oops", message = ?oops);
70
71    let mut framebuf = unsafe { super::framebuf::mk_framebuf() };
72    framebuf.fill(RgbColor::RED);
73
74    let mut target = framebuf.as_draw_target();
75    let uwu = MonoTextStyleBuilder::new()
76        .font(&ascii::FONT_9X15_BOLD)
77        .text_color(Rgb888::RED)
78        .background_color(Rgb888::WHITE)
79        .build();
80
81    let _ = Text::with_alignment(
82        oops.situation.header(),
83        Point::new(5, 15),
84        uwu,
85        Alignment::Left,
86    )
87    .draw(&mut target)
88    .unwrap();
89    drop(framebuf);
90
91    let mk_writer = TextWriterBuilder::default()
92        .starting_point(Point::new(5, 30))
93        .default_color(mycelium_trace::color::Color::BrightWhite)
94        .build(|| unsafe { super::framebuf::mk_framebuf() });
95    writeln!(
96        mk_writer.make_writer(),
97        "uwu mycelium did a widdle fucky-wucky!\n"
98    )
99    .unwrap();
100
101    match oops.situation {
102        OopsSituation::Fault {
103            kind,
104            details: Some(deets),
105            ..
106        } => writeln!(mk_writer.make_writer(), "a {kind} occurred: {deets}\n").unwrap(),
107        OopsSituation::Fault {
108            kind,
109            details: None,
110            ..
111        } => writeln!(mk_writer.make_writer(), "a {kind} occurred!\n").unwrap(),
112        OopsSituation::Panic(panic) => {
113            let mut writer = mk_writer.make_writer();
114            write!(writer, "mycelium panicked: {}", panic.message()).unwrap();
115            if let Some(loc) = panic.location() {
116                writeln!(writer, "at {}:{}:{}", loc.file(), loc.line(), loc.column()).unwrap();
117            }
118        }
119        OopsSituation::AllocError(layout) => {
120            let mut writer = mk_writer.make_writer();
121            writeln!(
122                writer,
123                "out of memory: failed to allocate {}B aligned on a {}B boundary",
124                layout.size(),
125                layout.align()
126            )
127            .unwrap();
128        }
129    }
130
131    if let Some(registers) = oops.situation.registers() {
132        {
133            let mut writer = mk_writer.make_writer();
134            writeln!(
135                writer,
136                "%rip    = {:#016x}",
137                registers.instruction_ptr.as_usize()
138            )
139            .unwrap();
140            writeln!(writer, "%rsp    = {:#016x}", registers.stack_ptr.as_usize()).unwrap();
141            writeln!(writer, "%rflags = {:#016b}", registers.cpu_flags).unwrap();
142            writeln!(writer, "%cs     = {:?}", registers.code_segment).unwrap();
143            writeln!(writer, "%ss     = {:?}", registers.stack_segment).unwrap();
144        }
145
146        tracing::debug!(target: "oops", instruction_ptr = ?registers.instruction_ptr);
147        tracing::debug!(target: "oops", stack_ptr = ?registers.stack_ptr);
148        tracing::debug!(target: "oops", code_segment = ?registers.code_segment);
149        tracing::debug!(target: "oops", stack_segment = ?registers.stack_segment);
150        tracing::debug!(target: "oops", cpu_flags = ?fmt::bin(registers.cpu_flags));
151    }
152
153    // control regs
154    {
155        let mut writer = mk_writer.make_writer();
156
157        let cr0 = control_regs::Cr0::read();
158        writeln!(&mut writer, "%cr0    = {:#}", cr0.display_set_bits()).unwrap();
159        let cr2 = control_regs::Cr2::read();
160        writeln!(&mut writer, "%cr2    = {cr2:?}").unwrap();
161        let (cr3_page, cr3_flags) = control_regs::cr3::read();
162        writeln!(&mut writer, "%cr3    = {cr3_page:?} ({cr3_flags:?})").unwrap();
163        let cr4 = control_regs::Cr4::read();
164        writeln!(&mut writer, "%cr4    = {:#}", cr4.display_set_bits()).unwrap();
165    }
166
167    if let Some(registers) = oops.situation.registers() {
168        // skip printing disassembly if we already faulted; disassembling the
169        // fault address may fault a second time.
170        if !oops.already_faulted {
171            let fault_addr = registers.instruction_ptr.as_usize();
172            disassembly(fault_addr, &mk_writer);
173        }
174    }
175
176    // we were in the allocator, so dump the allocator's free list
177    if oops.involves_allocator() {
178        let alloc_state = oops.alloc;
179
180        let mut writer = mk_writer.make_writer();
181        if alloc_state.allocating > 0 {
182            writeln!(
183                &mut writer,
184                "...while allocating ({} allocations in progress)!",
185                alloc_state.allocating,
186            )
187            .unwrap();
188        }
189
190        if alloc_state.deallocating > 0 {
191            writeln!(
192                &mut writer,
193                "...while deallocating ({} deallocations in progress)!",
194                alloc_state.deallocating
195            )
196            .unwrap();
197        }
198
199        writer.write_char('\n').unwrap();
200        writeln!(&mut writer, "{alloc_state}").unwrap();
201
202        crate::ALLOC.dump_free_lists();
203    }
204
205    if oops.already_panicked {
206        writeln!(
207            mk_writer.make_writer(),
208            "...while handling a panic! we really screwed up!"
209        )
210        .unwrap();
211    }
212
213    if oops.already_faulted {
214        writeln!(
215            mk_writer.make_writer(),
216            "...while handling a fault! seems real bad lol!"
217        )
218        .unwrap();
219    }
220
221    writeln!(
222        mk_writer.make_writer(),
223        "\nit will NEVER be safe to turn off your computer!"
224    )
225    .unwrap();
226
227    #[cfg(test)]
228    oops.fail_test();
229
230    #[cfg(not(test))]
231    unsafe {
232        cpu::halt()
233    }
234}
235
236// === impl Oops ===
237
238static IS_PANICKING: AtomicBool = AtomicBool::new(false);
239static IS_FAULTING: AtomicBool = AtomicBool::new(false);
240
241impl<'a> Oops<'a> {
242    #[inline(always)] // don't push a stack frame in case we overflowed!
243    pub(super) fn fault(fault: Fault<'a>, kind: &'static str) -> Self {
244        let situation = OopsSituation::Fault {
245            kind,
246            fault,
247            details: None,
248        };
249        Self::mk_fault(situation)
250    }
251
252    #[inline(always)] // don't push a stack frame in case we overflowed!
253    pub(super) fn fault_with_details(
254        fault: Fault<'a>,
255        kind: &'static str,
256        details: &'a dyn fmt::Display,
257    ) -> Self {
258        let situation = OopsSituation::Fault {
259            kind,
260            fault,
261            details: Some(details),
262        };
263        Self::mk_fault(situation)
264    }
265
266    #[inline(always)]
267    fn mk_fault(situation: OopsSituation<'a>) -> Self {
268        let already_panicked = IS_PANICKING.load(Ordering::Acquire);
269        let already_faulted = IS_FAULTING.swap(true, Ordering::AcqRel);
270        Self {
271            alloc: crate::ALLOC.state(),
272            already_panicked,
273            already_faulted,
274            situation,
275        }
276    }
277
278    pub fn panic(panic: &'a PanicInfo<'a>) -> Self {
279        let already_panicked = IS_PANICKING.swap(true, Ordering::AcqRel);
280        let already_faulted = IS_FAULTING.load(Ordering::Acquire);
281        Self {
282            alloc: crate::ALLOC.state(),
283            already_panicked,
284            already_faulted,
285            situation: OopsSituation::Panic(panic),
286        }
287    }
288
289    pub fn alloc_error(layout: alloc::alloc::Layout) -> Self {
290        let already_panicked = IS_PANICKING.swap(true, Ordering::AcqRel);
291        let already_faulted = IS_FAULTING.load(Ordering::Acquire);
292        Self {
293            alloc: crate::ALLOC.state(),
294            already_panicked,
295            already_faulted,
296            situation: OopsSituation::AllocError(layout),
297        }
298    }
299
300    fn involves_allocator(&self) -> bool {
301        matches!(self.situation, OopsSituation::AllocError(_)) || self.alloc.in_allocator()
302    }
303
304    #[cfg(test)]
305    fn fail_test(&self) -> ! {
306        use super::{qemu_exit, QemuExitCode};
307        let failure = match self.situation {
308            OopsSituation::Panic(_) | OopsSituation::AllocError(_) => mycotest::Failure::Panic,
309            OopsSituation::Fault { .. } => mycotest::Failure::Fault,
310        };
311
312        if let Some(test) = mycotest::runner::current_test() {
313            if let Some(com1) = serial::com1() {
314                // if writing the test outcome fails, don't double panic...
315                let _ = test.write_outcome(Err(failure), com1.lock());
316            }
317        }
318        qemu_exit(QemuExitCode::Failed)
319    }
320}
321
322impl<'a> From<&'a PanicInfo<'a>> for Oops<'a> {
323    #[inline]
324    fn from(panic: &'a PanicInfo<'a>) -> Self {
325        Self::panic(panic)
326    }
327}
328
329// === impl OopsSituation ===
330
331impl OopsSituation<'_> {
332    fn header(&self) -> &'static str {
333        match self {
334            Self::Fault { .. } => " OOPSIE-WOOPSIE! ",
335            Self::Panic(_) => " DON'T PANIC! ",
336            Self::AllocError(_) => " ALLOCATOR MACHINE BROKE! ",
337        }
338    }
339
340    fn registers(&self) -> Option<&X64Registers> {
341        match self {
342            Self::Fault { fault, .. } => Some(fault.registers()),
343            _ => None,
344        }
345    }
346}
347
348impl fmt::Debug for OopsSituation<'_> {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        match self {
351            Self::Fault {
352                kind,
353                fault,
354                details,
355            } => {
356                let mut dbg = f.debug_struct("OopsSituation::Fault");
357                dbg.field("kind", kind)
358                    .field("registers", fault.registers());
359                if let Some(deets) = details {
360                    dbg.field("details", &format_args!("\"{deets}\""));
361                }
362                dbg.finish()
363            }
364            Self::Panic(panic) => f.debug_tuple("OopsSituation::Panic").field(&panic).finish(),
365            Self::AllocError(layout) => f
366                .debug_tuple("OopsSituation::AllocError")
367                .field(&layout)
368                .finish(),
369        }
370    }
371}
372
373#[tracing::instrument(target = "oops", level = "trace", skip(rip, mk_writer), fields(rip = fmt::hex(rip)))]
374#[inline(always)]
375fn disassembly<'a>(rip: usize, mk_writer: &'a impl MakeWriter<'a>) {
376    use yaxpeax_arch::LengthedInstruction;
377    let _ = writeln!(mk_writer.make_writer(), "\nDisassembly:\n");
378    let mut ptr = rip as u64;
379    let decoder = yaxpeax_x86::long_mode::InstDecoder::default();
380    for i in 0..10 {
381        // Safety: who cares! At worst this might double-fault by reading past the end of valid
382        // memory. whoopsie.
383        let bytes = unsafe { core::slice::from_raw_parts(ptr as *const u8, 16) };
384        let indent = if i == 0 { "> " } else { "  " };
385        let _ = write!(mk_writer.make_writer(), "{indent}{ptr:016x}: ");
386        match decoder.decode_slice(bytes) {
387            Ok(inst) => {
388                let _ = writeln!(mk_writer.make_writer(), "{inst}");
389                tracing::debug!(target: "oops", "{ptr:016x}: {inst}");
390                ptr += inst.len();
391            }
392            Err(e) => {
393                let _ = writeln!(mk_writer.make_writer(), "{e}");
394                tracing::debug!(target: "oops", "{ptr:016x}: {e}");
395                break;
396            }
397        }
398    }
399}