Skip to content

Commit e7f5f92

Browse files
hawkwmkeetercbiffle
authored
Add read_panic_message kipc (#2313)
Currently, there is no way to programmatically access the panic message of a task which has faulted due to a Rust panic fron within the Hubris userspace. This branch adds a new `read_panic_message` kipc that copies the contents of a panicked task's panic message buffer into the caller. If the requested task has not panicked, this kipc returns an error indicating this. This is intended by use by supervisor implementations or other tasks which wish to report panic messages from userspace. I've also added a test case that exercises this functionality. Fixes #2311 --------- Co-authored-by: Matt Keeter <[email protected]> Co-authored-by: Cliff L. Biffle <[email protected]>
1 parent fe45fd6 commit e7f5f92

File tree

6 files changed

+269
-14
lines changed

6 files changed

+269
-14
lines changed

doc/kipc.adoc

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ This interface is primarily intended for testing interrupt handling, and can
332332
only be called by a task running as the supervisor. When this KIPC is called,
333333
an actual machine interrupt is triggered for the relevant hardware interrupt
334334
source. The kernel then responds to the interrupt using its normal
335-
interrupt-handling infrastructure, which dispatches a notification to the
335+
interrupt-handling infrastructure, which dispatches a notification to the
336336
subscribed task.
337337

338338
This KIPC is *not* intended for use as a general-purpose inter-task signalling
@@ -402,6 +402,55 @@ while let Some(fault) = kipc::find_faulted_task(next_task) {
402402
}
403403
----
404404

405+
=== `read_panic_message` (10)
406+
407+
If the task with the requested index is in the faulted state due to a panic,
408+
reads the contents of its panic message into the response buffer.
409+
410+
==== Request
411+
412+
[source,rust]
413+
----
414+
struct ReadPanicMessageRequest {
415+
task_index: u32,
416+
}
417+
----
418+
419+
==== Preconditions
420+
421+
`task_index` must be a valid task index for this system.
422+
423+
==== Response
424+
425+
[source,rust]
426+
----
427+
Result<&[u8], abi::ReadPanicMessageError>
428+
----
429+
430+
==== Notes
431+
432+
If the requested task is not currently in the faulted state due to a panic,
433+
this KIPC returns the `abi::ReadPanicMessageError::TaskNotPanicked` response
434+
code.
435+
436+
If the requested task has panicked, but the buffer containing its panic
437+
message is invalid, this KIPC returns the
438+
`abi::ReadPanicMessageError::BadPanicMessage` response code. If the target task
439+
uses the panic handler provided by the `userlib` crate, this should not be
440+
possible. However, it may occur if a task calls the `sys_panic` syscall
441+
directly with invalid arguments, such as a slice pointing outside the task's memory.
442+
443+
The panic message is truncated to the length of the response buffer. Since
444+
Hubris only stores the first 128 bytes of panic messages, a 128-byte response
445+
buffer will normally not result in further truncation.
446+
447+
The panic message is normally UTF-8. However, because of this byte-based
448+
truncation, and because the panic message is coming from a failing task in the
449+
first place, be careful not to *assume* that the panic message is valid UTF-8.
450+
See the
451+
link:++https://doc.rust-lang.org/stable/std/primitive.slice.html#method.utf8_chunks++[`<[u8]>::utf8_chunks`]
452+
function for one possible method of handling the panic message safely.
453+
405454
== Receiving from the kernel
406455

407456
The kernel never sends messages to tasks. It's simply not equipped to do so.

sys/abi/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ pub enum Kipcnum {
512512
ReadTaskDumpRegion = 7,
513513
SoftwareIrq = 8,
514514
FindFaultedTask = 9,
515+
ReadPanicMessage = 10,
515516
}
516517

517518
impl core::convert::TryFrom<u16> for Kipcnum {
@@ -528,6 +529,7 @@ impl core::convert::TryFrom<u16> for Kipcnum {
528529
7 => Ok(Self::ReadTaskDumpRegion),
529530
8 => Ok(Self::SoftwareIrq),
530531
9 => Ok(Self::FindFaultedTask),
532+
10 => Ok(Self::ReadPanicMessage),
531533
_ => Err(()),
532534
}
533535
}
@@ -582,3 +584,35 @@ bitflags::bitflags! {
582584
const CLEAR_PENDING = 1 << 1;
583585
}
584586
}
587+
588+
/// Errors returned by [`Kipcnum::ReadPanicMessage`].
589+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
590+
#[repr(u32)]
591+
pub enum ReadPanicMessageError {
592+
/// The task in question has not panicked.
593+
TaskNotPanicked = 1,
594+
/// The task has panicked, but its panic message buffer is invalid, so the
595+
/// kernel has not let us have it.
596+
///
597+
/// In practice, this is quite unlikely, and would require the task to have
598+
/// panicked with a panic message slice of a length that exceeds the end of
599+
/// the address space. Panicking via the Hubris userlib will never do this.
600+
/// But, since the panicked task could be any arbitrary binary...anything is
601+
/// possible.
602+
BadPanicBuffer = 2,
603+
}
604+
605+
/// We're using an explicit `TryFrom` impl for `ReadPanicMessageError` instead of
606+
/// `FromPrimitive` because the kernel doesn't currently depend on `num-traits`
607+
/// and this seems okay.
608+
impl core::convert::TryFrom<u32> for ReadPanicMessageError {
609+
type Error = ();
610+
611+
fn try_from(x: u32) -> Result<Self, Self::Error> {
612+
match x {
613+
1 => Ok(Self::TaskNotPanicked),
614+
2 => Ok(Self::BadPanicBuffer),
615+
_ => Err(()),
616+
}
617+
}
618+
}

sys/kern/src/kipc.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use unwrap_lite::UnwrapLite;
1111
use crate::arch;
1212
use crate::err::UserError;
1313
use crate::task::{current_id, ArchState, NextTask, Task};
14-
use crate::umem::USlice;
14+
use crate::umem::{safe_copy, USlice};
1515

1616
/// Message dispatcher.
1717
pub fn handle_kernel_message(
@@ -43,6 +43,9 @@ pub fn handle_kernel_message(
4343
Ok(Kipcnum::FindFaultedTask) => {
4444
find_faulted_task(tasks, caller, args.message?, args.response?)
4545
}
46+
Ok(Kipcnum::ReadPanicMessage) => {
47+
read_panic_message(tasks, caller, args.message?, args.response?)
48+
}
4649

4750
_ => {
4851
// Task has sent an unknown message to the kernel. That's bad.
@@ -507,3 +510,75 @@ fn find_faulted_task(
507510
.set_send_response_and_length(0, response_len);
508511
Ok(NextTask::Same)
509512
}
513+
514+
fn read_panic_message(
515+
tasks: &mut [Task],
516+
caller: usize,
517+
message: USlice<u8>,
518+
response: USlice<u8>,
519+
) -> Result<NextTask, UserError> {
520+
let index: u32 = deserialize_message(&tasks[caller], message)?;
521+
let index = index as usize;
522+
let Some(task) = tasks.get(index) else {
523+
return Err(UserError::Unrecoverable(FaultInfo::SyscallUsage(
524+
UsageError::TaskOutOfRange,
525+
)));
526+
};
527+
528+
// Make sure the task is actually panicked.
529+
let TaskState::Faulted {
530+
fault: FaultInfo::Panic,
531+
..
532+
} = task.state()
533+
else {
534+
return Err(UserError::Recoverable(
535+
abi::ReadPanicMessageError::TaskNotPanicked as u32,
536+
NextTask::Same,
537+
));
538+
};
539+
540+
let Ok(message) = task.save().as_panic_args().message else {
541+
// There's really only one reason that `as_panic_args().message` would
542+
// be an error. Because it's just a `USlice<u8>`, it can't be
543+
// misaligned, so the only possible invalid slice here is one whose
544+
// length exceeds the size of the address space, so that `base + len`
545+
// would overflow.
546+
//
547+
// But, we shouldn't fault the *caller* over that; they didn't do it!
548+
return Err(UserError::Recoverable(
549+
abi::ReadPanicMessageError::BadPanicBuffer as u32,
550+
NextTask::Same,
551+
));
552+
};
553+
554+
// Note that if the panic was recorded by `userlib`'s panic handler, it will
555+
// never exceed 128 bytes in length, and if the caller requested this kipc
556+
// using the `userlib::ipc::read_panic_message()` wrapper, then the caller's
557+
// buffer will always be exactly 128 bytes long. However, we can't rely on
558+
// that here, as either task *could* be an arbitrary binary that wasn't
559+
// compiled with the Hubris userlib, so we need to be safe regardless.
560+
match safe_copy(tasks, index, message, caller, response) {
561+
Ok(len) => {
562+
// Ladies and gentlemen...we got him!
563+
tasks[caller]
564+
.save_mut()
565+
.set_send_response_and_length(0, len);
566+
567+
Ok(NextTask::Same)
568+
}
569+
Err(crate::err::InteractFault {
570+
dst: Some(fault), ..
571+
}) => {
572+
// If the caller's buffer was invalid, they take a fault.
573+
Err(UserError::Unrecoverable(fault))
574+
}
575+
Err(_) => {
576+
// Source region was bad, but it's not the caller's fault; give them
577+
// a recoverable error.
578+
Err(UserError::Recoverable(
579+
abi::ReadPanicMessageError::BadPanicBuffer as u32,
580+
NextTask::Same,
581+
))
582+
}
583+
}
584+
}

sys/userlib/src/kipc.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
//! wastes flash space in the supervisor.
1616
1717
use core::num::NonZeroUsize;
18+
use core::str::Utf8Chunks;
1819

19-
use abi::{Kipcnum, TaskId};
20+
use abi::{Kipcnum, ReadPanicMessageError, TaskId};
2021
use zerocopy::IntoBytes;
2122

22-
use crate::{sys_send, UnwrapLite};
23+
use crate::{sys_send, UnwrapLite, PANIC_MESSAGE_MAX_LEN};
2324

2425
pub fn read_task_status(task: usize) -> abi::TaskState {
2526
// Coerce `task` to a known size (Rust doesn't assume that usize == u32)
@@ -162,3 +163,59 @@ pub fn software_irq(task: usize, mask: u32) {
162163
&[],
163164
);
164165
}
166+
167+
/// Reads a task's panic message into the provided `buf`, if the task is
168+
/// panicked.
169+
///
170+
/// Note that Hubris normally only preserves the first
171+
/// [`PANIC_MESSAGE_MAX_LEN`] bytes of a task's panic message, and panic
172+
/// messages greater than that length are truncated. Thus, this function
173+
/// accepts a buffer of that length.
174+
///
175+
/// # Returns
176+
///
177+
/// - [`Ok`]`([`Utf8Chunks`])` if the task is panicked.
178+
///
179+
/// The returned [`Utf8Chunks`] is an iterator returning a
180+
/// [`core::str::Utf8Chunk`] for each contiguous chunk of valid or invalid
181+
/// UTF-8 bytes in the panicked task's panic message buffer. This is due to
182+
/// the truncation of panic messages to [`PANIC_MESSAGE_MAX_LEN`] bytes,
183+
/// which may occur inside of a code point. If the panic message is truncated
184+
/// within a code point, there will be an invalid byte sequence at the end of
185+
/// the buffer, and the `Utf8Chunks` iterator allows the caller to select
186+
/// only the valid Unicode portion of the message. Provided that the panicked
187+
/// task panicked using the Hubris userlib's panic handler, the iterator will
188+
/// contain a single valid UTF-8 chunk, which may be followed by up to one
189+
/// invalid chunk. However, the task may potentially have called the panic
190+
/// syscall through other means, and therefore, there may be multiple valid
191+
/// chunks interspersed with invalid bytes.
192+
///
193+
/// The total byte length of all chunks in the iterator will be at most
194+
/// [`PANIC_MESSAGE_MAX_LEN`] bytes. Note that the iterator may contain only
195+
/// a single zero-length chunk, if the task has panicked but was compiled
196+
/// without panic messages enabled.
197+
/// - [`Err`]`(`[`ReadPanicMessageError::TaskNotPanicked`]`)` if the task is
198+
/// not currently faulted due to a panic.
199+
/// - [`Err`]`(`[`ReadPanicMessageError::BadPanicMessage`]`)` if the task has
200+
/// panicked but the panic message buffer is invalid to read from.
201+
pub fn read_panic_message(
202+
task: usize,
203+
buf: &mut [u8; PANIC_MESSAGE_MAX_LEN],
204+
) -> Result<Utf8Chunks<'_>, ReadPanicMessageError> {
205+
let task = task as u32;
206+
let (rc, len) = sys_send(
207+
TaskId::KERNEL,
208+
Kipcnum::ReadPanicMessage as u16,
209+
task.as_bytes(),
210+
&mut buf[..],
211+
&[],
212+
);
213+
214+
if rc == 0 {
215+
Ok(buf[..len].utf8_chunks())
216+
} else {
217+
// If the kernel sent us an unknown response code....i dunno, guess
218+
// i'll die?
219+
Err(ReadPanicMessageError::try_from(rc).unwrap_lite())
220+
}
221+
}

sys/userlib/src/lib.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,9 +1402,19 @@ compile_error!(
14021402
this check in userlib.)"
14031403
);
14041404

1405+
/// Maximum length (in bytes) of the panic message string captured when the
1406+
/// `panic-messages` feature is enabled. Panics which format messages longer
1407+
/// than this many bytes are truncated to this length.
1408+
///
1409+
/// There's a tradeoff here between "getting a useful message" and "wasting a
1410+
/// lot of RAM." Somewhat arbitrarily, we choose to collect this many bytes
1411+
/// of panic message (and permanently reserve the same number of bytes of
1412+
/// RAM):
1413+
pub const PANIC_MESSAGE_MAX_LEN: usize = 128;
1414+
14051415
/// Panic handler for user tasks with the `panic-messages` feature enabled. This
14061416
/// handler will try its best to generate a panic message, up to a maximum
1407-
/// buffer size (configured below).
1417+
/// of [`PANIC_MESSAGE_MAX_LEN`] bytes.
14081418
///
14091419
/// Including this panic handler permanently reserves a buffer in the RAM of a
14101420
/// task, to ensure that memory is available for the panic message, even if the
@@ -1423,13 +1433,7 @@ fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
14231433
//
14241434
// There is unfortunately no way to have the compiler _check_ that the code
14251435
// does not panic, so we have to work very carefully.
1426-
1427-
// There's a tradeoff here between "getting a useful message" and "wasting a
1428-
// lot of RAM." Somewhat arbitrarily, we choose to collect this many bytes
1429-
// of panic message (and permanently reserve the same number of bytes of
1430-
// RAM):
1431-
const BUFSIZE: usize = 128;
1432-
1436+
//
14331437
// Panic messages get constructed using `core::fmt::Write`. If we implement
14341438
// that trait, we can provide our own type that will back the
14351439
// `core::fmt::Formatter` handed into any formatting routines (like those on
@@ -1444,7 +1448,7 @@ fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
14441448
/// Content will be written here. While the content itself will be
14451449
/// UTF-8, it may end in an incomplete UTF-8 character to simplify our
14461450
/// truncation logic.
1447-
buf: &'static mut [u8; BUFSIZE],
1451+
buf: &'static mut [u8; PANIC_MESSAGE_MAX_LEN],
14481452
/// Number of bytes of `buf` that are valid.
14491453
///
14501454
/// Invariant: always in the range `0..buf.len()`.
@@ -1520,7 +1524,8 @@ fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
15201524

15211525
// We declare a single static panic buffer per task, to ensure the memory is
15221526
// available.
1523-
static mut PANIC_BUFFER: [u8; BUFSIZE] = [0; BUFSIZE];
1527+
static mut PANIC_BUFFER: [u8; PANIC_MESSAGE_MAX_LEN] =
1528+
[0; PANIC_MESSAGE_MAX_LEN];
15241529

15251530
// Okay. Now we start the actual panicking process.
15261531
//

test/test-suite/src/main.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ test_cases! {
134134
test_irq_status,
135135
#[cfg(feature = "fru-id-eeprom")]
136136
at24csw080::test_at24csw080,
137+
test_read_panic_message,
137138
}
138139

139140
/// Tests that we can send a message to our assistant, and that the assistant
@@ -1553,6 +1554,40 @@ fn test_irq_status() {
15531554
assert_eq!(status, expected_status);
15541555
}
15551556

1557+
/// Tests that when a task panics, its panic message can be read via the `read_panic_message` kipc.
1558+
fn test_read_panic_message() {
1559+
set_autorestart(false);
1560+
1561+
let mut buf = [0u8; userlib::PANIC_MESSAGE_MAX_LEN];
1562+
1563+
match kipc::read_panic_message(ASSIST.get_task_index().into(), &mut buf) {
1564+
Err(userlib::ReadPanicMessageError::TaskNotPanicked) => {}
1565+
x => panic!("expected `Err(TaskNotPanicked)`, got: {x:?}"),
1566+
}
1567+
1568+
// Ask the assistant to panic.
1569+
let assist = assist_task_id();
1570+
let mut response = 0u32;
1571+
let (_, _) = userlib::sys_send(
1572+
assist,
1573+
AssistOp::Panic as u16,
1574+
&0u32.to_le_bytes(),
1575+
response.as_mut_bytes(),
1576+
&[],
1577+
);
1578+
1579+
let mut msg_chunks =
1580+
kipc::read_panic_message(ASSIST.get_task_index().into(), &mut buf)
1581+
.unwrap();
1582+
// it should look kinda like a panic message (but since the line number may
1583+
// change, don't make assertions about the entire contents of the string...
1584+
let msg = msg_chunks
1585+
.next()
1586+
.expect("Utf8Chunks always has at least one chunk")
1587+
.valid();
1588+
assert!(msg.starts_with("panicked at"));
1589+
}
1590+
15561591
/// Asks the test runner (running as supervisor) to please trigger a software
15571592
/// interrupt for `notifications::TEST_IRQ`, thank you.
15581593
#[track_caller]

0 commit comments

Comments
 (0)