blob: ac77b63d57fe4b406806c7a8a886d286f17c78c6 [file]
// Copyright 2026 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
use crate::arch_rs::{InterruptDisableGuard, curr_cpu_num, ints_disabled};
use crate::kernel::thread::{FxtRef, ThreadPtr};
pub use crate::kernel::types::Koid;
pub use crate::platform_rs::timer::{InstantBootTicks, timer_current_boot_ticks};
use core::cell::UnsafeCell;
use core::mem::{MaybeUninit, size_of};
use core::ptr::NonNull;
use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering};
use core::{ffi, ptr, slice};
pub use fxt_layout::{
ArgumentHeader, ArgumentType, EventRecordHeader, EventType, KernelObjectRecordHeader,
LargeRecordHeader, RecordHeader, RecordType, StringRefHeader,
};
use kalloc::Box;
pub use kstring::declare_interned_category;
use kstring::declare_interned_string;
use kstring::interned_category::InternedCategory;
pub use kstring::interned_string::InternedString;
use spsc_buffer::{Buffer, NoOpAllocator, Reservation};
use zx_status::Status;
use zx_types::zx_status_t;
use crate::user_copy::UserOutPtr;
unsafe extern "C" {
fn cpp_ktrace_read_user(
ptr: UserOutPtr<u8>,
offset: u32,
len: usize,
out_actual: *mut usize,
) -> zx_status_t;
fn cpp_ktrace_control(action: u32, options: u32) -> zx_status_t;
}
// Re-export the ktrace macros from the sub-crate.
#[allow(unused_imports)]
pub use ktrace_macro::*;
// LINT.IfChange(KTraceState)
#[repr(C)]
pub struct KTraceState {
pub categories_bitmask: AtomicU32,
pub writes_enabled: AtomicBool,
}
const _: () = assert!(size_of::<KTraceState>() == 8);
// LINT.ThenChange(//zircon/kernel/include/lib/ktrace.h:KTraceState)
declare_interned_category!(META_CAT, "kernel:meta", extern);
declare_interned_string!(DROP_STATS_REF, "drop_stats", extern);
declare_interned_string!(NUM_RECORDS_REF, "num_records", extern);
declare_interned_string!(NUM_BYTES_REF, "num_bytes", extern);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Context {
Thread = 0,
Cpu = 1,
}
/// The value of a trace argument.
pub enum ArgValue<'a> {
Null,
Bool(bool),
Int32(i32),
Uint32(u32),
Int64(i64),
Uint64(u64),
Double(f64),
String(&'a str),
Pointer(usize),
Koid(u64),
}
impl<'a> From<bool> for ArgValue<'a> {
fn from(v: bool) -> Self {
ArgValue::Bool(v)
}
}
impl<'a> From<i32> for ArgValue<'a> {
fn from(v: i32) -> Self {
ArgValue::Int32(v)
}
}
impl<'a> From<u32> for ArgValue<'a> {
fn from(v: u32) -> Self {
ArgValue::Uint32(v)
}
}
impl<'a> From<i64> for ArgValue<'a> {
fn from(v: i64) -> Self {
ArgValue::Int64(v)
}
}
impl<'a> From<u64> for ArgValue<'a> {
fn from(v: u64) -> Self {
ArgValue::Uint64(v)
}
}
impl<'a> From<f64> for ArgValue<'a> {
fn from(v: f64) -> Self {
ArgValue::Double(v)
}
}
impl<'a> From<&'a str> for ArgValue<'a> {
fn from(v: &'a str) -> Self {
ArgValue::String(v)
}
}
/// A wrapper for KOID values passed as trace arguments.
impl<'a> From<Koid> for ArgValue<'a> {
fn from(v: Koid) -> Self {
ArgValue::Koid(v.0)
}
}
/// Represents an FXT string reference that can either be an interned string or an inline string byte slice.
#[derive(Clone, Copy)]
pub enum StringRef<'a> {
Interned(&'static InternedString),
Inline(&'a [u8]),
}
impl<'a> From<&'static InternedString> for StringRef<'a> {
fn from(s: &'static InternedString) -> Self {
StringRef::Interned(s)
}
}
impl<'a> From<&'a [u8]> for StringRef<'a> {
fn from(s: &'a [u8]) -> Self {
StringRef::Inline(s)
}
}
impl<'a> From<&'a str> for StringRef<'a> {
fn from(s: &'a str) -> Self {
StringRef::Inline(s.as_bytes())
}
}
impl<'a> StringRef<'a> {
/// Returns the FXT header entry for this string reference.
pub fn header_entry(&self) -> u16 {
match self {
StringRef::Interned(s) => {
let mut hdr = StringRefHeader::new();
hdr.set_id_or_len(s.id());
hdr.bits()
}
StringRef::Inline(b) => {
let len = b.len().min(0x7fff) as u16;
let mut hdr = StringRefHeader::new();
hdr.set_is_inline(true).set_id_or_len(len);
hdr.bits()
}
}
}
/// Returns the payload size in 64-bit words required to store this string inline.
pub fn payload_words(&self) -> usize {
match self {
StringRef::Interned(_) => 0,
StringRef::Inline(b) => {
let len = b.len().min(0x7fff);
len.div_ceil(8)
}
}
}
/// Writes the inline string payload if applicable.
pub fn write(&self, res: &mut KTraceReservation<'_>) -> Result<(), Status> {
match self {
StringRef::Interned(_) => Ok(()),
StringRef::Inline(b) => {
let len = b.len().min(0x7fff);
res.write_bytes(&b[..len])
}
}
}
}
pub struct Argument<'a> {
name: &'static InternedString,
value: ArgValue<'a>,
}
impl<'a> Argument<'a> {
pub fn new(name: &'static InternedString, value: impl Into<ArgValue<'a>>) -> Self {
Self { name, value: value.into() }
}
/// Returns the size of this argument in 64-bit words.
fn size_words(&self) -> usize {
match &self.value {
ArgValue::Null | ArgValue::Bool(_) | ArgValue::Int32(_) | ArgValue::Uint32(_) => 1,
ArgValue::Int64(_)
| ArgValue::Uint64(_)
| ArgValue::Double(_)
| ArgValue::Pointer(_)
| ArgValue::Koid(_) => 2,
ArgValue::String(s) => 1 + s.len().min(0x7fff).div_ceil(8),
}
}
fn write(&self, res: &mut KTraceReservation<'_>) -> Result<(), Status> {
let name_id = self.name.id();
let size_words = self.size_words() as u16;
match &self.value {
ArgValue::Null => {
let header = ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Null);
res.write_word(header.bits())?;
}
ArgValue::Int32(v) => {
let mut header =
ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Int32);
header.set_value_bits(*v as u32);
res.write_word(header.bits())?;
}
ArgValue::Uint32(v) => {
let mut header =
ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Uint32);
header.set_value_bits(*v);
res.write_word(header.bits())?;
}
ArgValue::Int64(v) => {
let header = ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Int64);
res.write_word(header.bits())?;
res.write_word(*v as u64)?;
}
ArgValue::Uint64(v) => {
let header =
ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Uint64);
res.write_word(header.bits())?;
res.write_word(*v)?;
}
ArgValue::Double(v) => {
let header =
ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Double);
res.write_word(header.bits())?;
res.write_word(v.to_bits())?;
}
ArgValue::String(s) => {
let mut header =
ArgumentHeader::for_argument(name_id, size_words, ArgumentType::String);
let string_len = s.len().min(0x7fff) as u16;
let mut str_ref = StringRefHeader::new();
str_ref.set_is_inline(true).set_id_or_len(string_len);
header.set_value_bits(str_ref.bits() as u32);
res.write_word(header.bits())?;
res.write_bytes(&s.as_bytes()[..string_len as usize])?;
}
ArgValue::Pointer(v) => {
let header =
ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Pointer);
res.write_word(header.bits())?;
res.write_word(*v as u64)?;
}
ArgValue::Koid(v) => {
let header = ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Koid);
res.write_word(header.bits())?;
res.write_word(*v)?;
}
ArgValue::Bool(v) => {
let mut header =
ArgumentHeader::for_argument(name_id, size_words, ArgumentType::Bool);
header.set_value_bits(*v as u32);
res.write_word(header.bits())?;
}
}
Ok(())
}
}
pub struct KTraceScope<'a> {
category: &'static InternedCategory,
name: &'static InternedString,
timestamp: InstantBootTicks,
context: Context,
args: &'a [Argument<'a>],
}
impl<'a> KTraceScope<'a> {
#[inline(never)]
#[cold]
pub fn begin(
category: &'static InternedCategory,
name: &'static InternedString,
context: Context,
args: &'a [Argument<'a>],
) -> Self {
let timestamp = timer_current_boot_ticks();
Self { category, name, timestamp, context, args }
}
}
impl<'a> Drop for KTraceScope<'a> {
#[inline(never)]
#[cold]
fn drop(&mut self) {
let end_time = timer_current_boot_ticks();
let ktrace = KTrace::get_instance();
ktrace.emit_event(
EventType::DurationComplete,
self.category,
self.name,
self.timestamp,
self.context,
Some(end_time.0 as u64),
self.args,
);
}
}
// LINT.IfChange(DroppedRecordDurationEvent)
#[repr(C)]
struct DroppedRecordDurationEvent {
header: u64,
start: InstantBootTicks,
process_id: u64,
thread_id: u64,
num_dropped_arg: u64,
bytes_dropped_arg: u64,
end: InstantBootTicks,
}
const _: () = assert!(size_of::<DroppedRecordDurationEvent>() == 56);
// LINT.ThenChange(//zircon/kernel/lib/percpu_writer/include/lib/percpu_writer/buffer.h:DroppedRecordDurationEvent)
// LINT.IfChange(DroppedRecordStats)
/// This struct keeps track of the duration, number, and size of trace records dropped when the
/// buffer is full. These statistics are emitted to the trace buffer as a duration as soon as
/// space is available to do so, at which point the values are reset to 0, or false in the case
/// of has_dropped.
#[repr(C)]
#[derive(Default, Debug, Clone)]
pub struct DroppedRecordStats {
pub first_dropped: InstantBootTicks,
pub last_dropped: InstantBootTicks,
/// By storing num_dropped and bytes_dropped in 32-bit values, we ensure that they can each
/// be stored in a single 64-bit word in the FXT record we emit when space is available.
pub num_dropped: u32,
pub bytes_dropped: u32,
pub has_dropped: bool,
}
const _: () = assert!(size_of::<DroppedRecordStats>() == 32);
// LINT.ThenChange(//zircon/kernel/lib/percpu_writer/include/lib/percpu_writer/buffer.h:DroppedRecordStats)
impl DroppedRecordStats {
pub fn reset(&mut self) {
self.first_dropped = InstantBootTicks(0);
self.last_dropped = InstantBootTicks(0);
self.num_dropped = 0;
self.bytes_dropped = 0;
self.has_dropped = false;
}
pub fn track(&mut self, now: InstantBootTicks, size: u32) {
if !self.has_dropped {
self.first_dropped = now;
self.has_dropped = true;
}
self.last_dropped = now;
self.num_dropped = self.num_dropped.wrapping_add(1);
self.bytes_dropped = self.bytes_dropped.wrapping_add(size);
}
pub fn has_dropped(&self) -> bool {
self.has_dropped
}
}
/// A Rust implementation of `percpu_writer::Buffer` that wraps an existing
/// `spsc_buffer::Buffer` and tracks dropped trace records.
pub struct KTraceBuffer {
// We use `NonNull` raw pointers instead of `&'static mut` because the underlying memory is
// allocated and owned by the C++ layer. Holding long-lived `&mut` references would assert
// perpetual exclusive ownership (`noalias`), violating Rust's aliasing rules across the FFI
// boundary. `NonNull` allows us to create bounded, localized references only when accessed
// with interrupts disabled.
buffer: NonNull<Buffer<NoOpAllocator>>,
drop_stats: NonNull<DroppedRecordStats>,
cpu_ref_header_entry: u16,
pub process_koid: u64,
pub thread_koid: u64,
}
// SAFETY: Access to the per-CPU buffers is synchronized by the caller (typically via disabling
// interrupts).
unsafe impl Send for KTraceBuffer {}
unsafe impl Sync for KTraceBuffer {}
impl KTraceBuffer {
/// Constructs a `KTraceBuffer` from raw pointers to an existing `spsc_buffer::Buffer`,
/// `DroppedRecordStats`, and its metadata.
pub fn new(
buffer: NonNull<Buffer<NoOpAllocator>>,
drop_stats: NonNull<DroppedRecordStats>,
cpu_ref_header_entry: u16,
process_koid: u64,
thread_koid: u64,
) -> Self {
Self { buffer, drop_stats, cpu_ref_header_entry, process_koid, thread_koid }
}
#[inline(always)]
fn buffer(&self) -> &Buffer<NoOpAllocator> {
unsafe { self.buffer.as_ref() }
}
#[inline(always)]
fn buffer_mut<'a>(&mut self) -> &'a mut Buffer<NoOpAllocator> {
unsafe { &mut *self.buffer.as_ptr() }
}
#[inline(always)]
pub fn drop_stats(&self) -> &DroppedRecordStats {
unsafe { self.drop_stats.as_ref() }
}
#[inline(always)]
pub fn drop_stats_mut<'a>(&mut self) -> &'a mut DroppedRecordStats {
unsafe { &mut *self.drop_stats.as_ptr() }
}
/// Returns the size of the backing storage.
pub fn size(&self) -> u32 {
self.buffer().size()
}
/// Drains the underlying Buffer.
pub fn drain(&self) -> Result<(), Status> {
self.buffer().drain()
}
/// Copies `len` bytes out of the buffer using the provided `copy_fn`.
pub fn read<F>(&self, copy_fn: F, len: u32) -> Result<u32, Status>
where
F: FnMut(u32, &[u8]) -> Result<(), Status>,
{
self.buffer().read(copy_fn, len)
}
/// Reserves a block of the given size in the buffer, interposing to write dropped record
/// statistics if any were tracked.
pub fn reserve(&mut self, header: u64) -> Result<KTraceReservation<'_>, Status> {
debug_assert!(ints_disabled());
// Compute the number of bytes we need to reserve from the provided fxt header.
let rec_hdr = RecordHeader::from(header);
let num_words = if rec_hdr.record_type() == RecordType::LargeBlob {
// Large record
LargeRecordHeader::from(header).record_size()
} else {
// Normal record
rec_hdr.record_size() as u32
};
let size = num_words.checked_mul(8).ok_or(Status::INVALID_ARGS)?;
let mut total_size = size;
let event = if self.drop_stats().has_dropped() {
total_size += size_of::<DroppedRecordDurationEvent>() as u32;
Some(self.serialize_drop_stats())
} else {
None
};
match self.buffer_mut().reserve(total_size) {
Err(status) => {
let now = timer_current_boot_ticks();
self.drop_stats_mut().track(now, size);
Err(status)
}
Ok(mut res) => {
if let Some(event) = event {
// SAFETY: DroppedRecordDurationEvent is repr(C) and has a defined binary
// layout.
let event_bytes = unsafe {
slice::from_raw_parts(
ptr::from_ref(&event).cast::<u8>(),
size_of::<DroppedRecordDurationEvent>(),
)
};
res.write(event_bytes)?;
self.drop_stats_mut().reset();
}
KTraceReservation::new(res, header)
}
}
}
/// Emit the dropped record stats to the trace buffer if we're currently tracking them.
pub fn emit_drop_stats(&mut self) -> Result<(), Status> {
debug_assert!(ints_disabled());
if !self.drop_stats().has_dropped() {
return Ok(());
}
// Serialize the event first so we release the borrow on self before calling reserve.
let event = self.serialize_drop_stats();
let mut res = self.buffer_mut().reserve(size_of::<DroppedRecordDurationEvent>() as u32)?;
// SAFETY: DroppedRecordDurationEvent is repr(C) and has a defined binary layout.
let event_bytes = unsafe {
slice::from_raw_parts(
ptr::from_ref(&event).cast::<u8>(),
size_of::<DroppedRecordDurationEvent>(),
)
};
res.write(event_bytes)?;
res.commit()?;
self.drop_stats_mut().reset();
Ok(())
}
fn serialize_drop_stats(&self) -> DroppedRecordDurationEvent {
let record_size_words = (size_of::<DroppedRecordDurationEvent>() / 8) as u16;
let mut header = EventRecordHeader::default();
header
.set_record_size(record_size_words)
.set_event_type(EventType::DurationComplete)
.set_arg_count(2)
.set_thread_ref(self.cpu_ref_header_entry as u8)
.set_category_ref(META_CAT.label().id())
.set_name_ref(DROP_STATS_REF.id());
let drop_stats = self.drop_stats();
let mut num_dropped_arg =
ArgumentHeader::for_argument(NUM_RECORDS_REF.id(), 1, ArgumentType::Uint32);
num_dropped_arg.set_value_bits(drop_stats.num_dropped);
let mut bytes_dropped_arg =
ArgumentHeader::for_argument(NUM_BYTES_REF.id(), 1, ArgumentType::Uint32);
bytes_dropped_arg.set_value_bits(drop_stats.bytes_dropped);
DroppedRecordDurationEvent {
header: header.bits(),
start: drop_stats.first_dropped,
process_id: self.process_koid,
thread_id: self.thread_koid,
num_dropped_arg: num_dropped_arg.bits(),
bytes_dropped_arg: bytes_dropped_arg.bits(),
end: drop_stats.last_dropped,
}
}
}
/// KTraceReservation encapsulates a pending write to the buffer.
#[derive(Debug)]
pub struct KTraceReservation<'a> {
reservation: Reservation<'a>,
}
impl<'a> KTraceReservation<'a> {
fn new(reservation: Reservation<'a>, header: u64) -> Result<Self, Status> {
let mut this = Self { reservation };
if let Err(e) = this.write_word(header) {
this.reservation.cancel();
return Err(e);
}
Ok(this)
}
/// Writes a single 64-bit word into the reservation.
pub fn write_word(&mut self, word: u64) -> Result<(), Status> {
debug_assert!(ints_disabled());
self.reservation.write(&word.to_ne_bytes())
}
/// Writes a byte slice into the reservation, padding to an 8-byte boundary.
pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), Status> {
debug_assert!(ints_disabled());
self.reservation.write(bytes)?;
let num_bytes = bytes.len();
let aligned_bytes = (num_bytes + 7) & !7;
let num_zeros_to_write = aligned_bytes - num_bytes;
if num_zeros_to_write > 0 {
let zero = [0u8; 8];
self.reservation.write(&zero[..num_zeros_to_write])?;
}
Ok(())
}
/// Commits the reservation, making it visible to the reader.
pub fn commit(self) -> Result<(), Status> {
debug_assert!(ints_disabled());
self.reservation.commit()
}
}
/// A pure Rust implementation of KTrace.
pub struct KTrace {
/// Reference to the shared C++ KTraceState.
// TODO(https://fxbug.dev/517305548): This should be made a direct allocation, and not a
// reference, once the C++ implementation is removed.
state: &'static KTraceState,
/// A heap-allocated slice of atomic pointers to per-CPU buffers.
/// Allocated once at boot time, and accessed completely lock-free on the hot path by CPU ID.
buffers: Box<[AtomicPtr<KTraceBuffer>]>,
}
// SAFETY: KTrace is a global singleton. Access to the per-CPU buffers is synchronized.
unsafe impl Sync for KTrace {}
unsafe impl Send for KTrace {}
#[repr(transparent)]
struct KTraceSingleton(UnsafeCell<MaybeUninit<KTrace>>);
unsafe impl Sync for KTraceSingleton {}
unsafe impl Send for KTraceSingleton {}
static INSTANCE: KTraceSingleton = KTraceSingleton(UnsafeCell::new(MaybeUninit::uninit()));
impl KTrace {
/// Retrieve the global instance of KTrace.
pub fn get_instance() -> &'static Self {
// SAFETY: KTrace must be initialized during kernel boot.
unsafe { &*INSTANCE.0.get().cast::<KTrace>() }
}
/// Returns the raw pointer to the KTraceBuffer for the given CPU.
pub fn get_buffer(&self, cpu: usize) -> *mut KTraceBuffer {
self.buffers[cpu].load(Ordering::Acquire)
}
/// Reserves a slot of memory to write a record into.
///
/// # Safety
///
/// This method MUST be invoked with interrupts disabled to enforce the single-writer invariant.
pub unsafe fn reserve(&self, header: u64) -> Result<KTraceReservation<'_>, Status> {
debug_assert!(ints_disabled());
if !self.writes_enabled() {
return Err(Status::BAD_STATE);
}
let cpu_num = curr_cpu_num() as usize;
if cpu_num >= self.buffers.len() {
return Err(Status::BAD_STATE);
}
let ptr = self.buffers[cpu_num].load(Ordering::Acquire);
if ptr.is_null() {
return Err(Status::BAD_STATE);
}
let buf = unsafe { &mut *ptr };
buf.reserve(header)
}
/// Returns true if writes are currently enabled.
pub fn writes_enabled(&self) -> bool {
self.state.writes_enabled.load(Ordering::Acquire)
}
/// Returns the categories bitmask.
pub fn categories_bitmask(&self) -> u32 {
self.state.categories_bitmask.load(Ordering::Acquire)
}
/// Returns true if the given category is enabled.
pub fn is_category_enabled(&self, category: &InternedCategory) -> bool {
let category_index = category.index();
if category_index == InternedCategory::INVALID_INDEX {
return false;
}
let bitmask = self.categories_bitmask();
(bitmask & (1 << category_index)) != 0
}
/// Reads up to `len` bytes from the ktrace buffer starting at `offset` into `ptr`.
///
/// Returns the number of bytes actually read.
pub fn read_user(&self, ptr: UserOutPtr<u8>, offset: u32, len: usize) -> Result<usize, Status> {
let mut actual = 0usize;
// SAFETY: `cpp_ktrace_read_user` reads trace data from the global ktrace buffer into the
// user buffer represented by `ptr` and writes the actual count into `actual`.
let status = unsafe { cpp_ktrace_read_user(ptr, offset, len, &mut actual) };
Status::ok(status)?;
Ok(actual)
}
/// Performs a control operation on the ktrace subsystem.
pub fn control(&self, action: u32, options: u32) -> Result<(), Status> {
// SAFETY: `cpp_ktrace_control` starts, stops, or rewinds tracing using validated action/options.
let status = unsafe { cpp_ktrace_control(action, options) };
Status::ok(status)
}
/// Low-level helper to write an FXT kernel object record.
///
/// This is not inlined to reduce code size at the instrumentation sites.
#[inline(never)]
#[cold]
pub fn emit_kernel_object_outlined<'a>(
&self,
koid: u64,
obj_type: u32,
name: impl Into<StringRef<'a>>,
args: &[Argument<'_>],
) {
let _guard = InterruptDisableGuard::new();
if !self.writes_enabled() {
return;
}
let name = name.into();
let base_size = 2; // Header, KOID
let name_size = name.payload_words();
let args = if args.len() > 15 { &args[..15] } else { args };
let args_size: usize = args.iter().map(|a| a.size_words()).sum();
let total_size_words = base_size + name_size + args_size;
if total_size_words > 0xfff {
return;
}
let mut header = KernelObjectRecordHeader::default();
header
.set_record_size(total_size_words as u16)
.set_obj_type(obj_type as u8)
.set_name_ref(name.header_entry())
.set_arg_count(args.len() as u8);
// SAFETY: Interrupts are disabled by `_guard`, guaranteeing mutual exclusion during reservation.
if let Ok(mut res) = unsafe { self.reserve(header.bits()) } {
let _ = res.write_word(koid);
let _ = name.write(&mut res);
for arg in args {
let _ = arg.write(&mut res);
}
let _ = res.commit();
}
}
/// Low-level helper to write a generic FXT event record.
///
/// This is not inlined to reduce code size at the instrumentation sites.
#[allow(clippy::too_many_arguments)]
#[inline(never)]
#[cold]
pub fn emit_event(
&self,
event_type: EventType,
category: &InternedCategory,
name: &InternedString,
timestamp: InstantBootTicks,
context: Context,
content: Option<u64>,
args: &[Argument<'_>],
) {
let _guard = InterruptDisableGuard::new();
if !self.writes_enabled() {
return;
}
// 1. Get the process/thread KOIDs for the context.
let (process_koid, thread_koid) = match context {
Context::Thread => {
// SAFETY: ktrace is initialized and running after threading has been initialized.
let FxtRef { pid, tid } = unsafe { ThreadPtr::current().fxt_ref() };
(pid, tid)
}
Context::Cpu => {
let cpu = curr_cpu_num() as usize;
if cpu >= self.buffers.len() {
return;
}
let ptr = self.buffers[cpu].load(Ordering::Acquire);
if ptr.is_null() {
return;
}
let buf = unsafe { &*ptr };
(buf.process_koid, buf.thread_koid)
}
};
// 2. Calculate the record size.
let base_size = 4; // Header, Timestamp, Process KOID, Thread KOID
let content_size = if content.is_some() { 1 } else { 0 };
let args = if args.len() > 15 { &args[..15] } else { args };
let args_size: usize = args.iter().map(|a| a.size_words()).sum();
let total_size_words = base_size + content_size + args_size;
if total_size_words > 0xfff {
return;
}
// 3. Construct the header.
let mut header = EventRecordHeader::default();
header
.set_record_size(total_size_words as u16)
.set_event_type(event_type)
.set_arg_count(args.len() as u8)
.set_category_ref(category.label().id())
.set_name_ref(name.id());
// 4. Reserve space and write the record.
if let Ok(mut res) = unsafe { self.reserve(header.bits()) } {
let _ = res.write_word(timestamp.0 as u64);
let _ = res.write_word(process_koid);
let _ = res.write_word(thread_koid);
for arg in args {
let _ = arg.write(&mut res);
}
if let Some(c) = content {
let _ = res.write_word(c);
}
let _ = res.commit();
}
}
}
/// Initializes the global KTrace instance with the given number of CPU buffers.
///
/// # Safety
///
/// This must be called at most once during kernel boot.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ktrace_init(num_buffers: u32, state_ptr: *mut ffi::c_void) -> i32 {
if num_buffers == 0 || state_ptr.is_null() {
return Status::INVALID_ARGS.into_raw();
}
// SAFETY: The caller guarantees that state_ptr points to a valid KTraceState instance
// which has static storage duration (lives forever) and is safe to access concurrently
// (since its fields are atomic).
let state = unsafe { &*state_ptr.cast::<KTraceState>() };
let buffers = match Box::<[AtomicPtr<KTraceBuffer>]>::try_new_zeroed_slice(num_buffers as usize)
{
Ok(b) => b,
Err(_) => return Status::NO_MEMORY.into_raw(),
};
let ktrace = KTrace { state, buffers };
unsafe {
let slot = INSTANCE.0.get();
ptr::write(slot.cast::<KTrace>(), ktrace);
}
zx_status::sys::ZX_OK
}
/// Initializes the KTraceBuffer for a specific CPU using a pointer to the C++ SpscBuffer.
///
/// # Safety
///
/// - `spsc_buffer_ptr` must point to a valid C++ `SpscBuffer` instance
/// which is binary-compatible with `spsc_buffer::Buffer<NoOpAllocator>`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ktrace_init_cpu_buffer(
cpu_num: u32,
spsc_buffer_ptr: *mut ffi::c_void,
drop_stats_ptr: *mut ffi::c_void,
process_koid: u64,
thread_koid: u64,
cpu_ref_header_entry: u16,
) -> i32 {
let ktrace = KTrace::get_instance();
if cpu_num >= ktrace.buffers.len() as u32 {
return Status::INVALID_ARGS.into_raw();
}
let buffer = match NonNull::new(spsc_buffer_ptr.cast::<Buffer<NoOpAllocator>>()) {
Some(b) => b,
None => return Status::INVALID_ARGS.into_raw(),
};
let drop_stats = match NonNull::new(drop_stats_ptr.cast::<DroppedRecordStats>()) {
Some(d) => d,
None => return Status::INVALID_ARGS.into_raw(),
};
// SAFETY: `spsc_buffer_ptr` is verified to be non-null above, and the caller guarantees that it
// points to a valid `SpscBuffer` whose memory layout is compatible with
// `Buffer<NoOpAllocator>`.
if !unsafe { buffer.as_ref().is_valid() } {
return Status::BAD_STATE.into_raw();
}
// Allocate the KTraceBuffer on the heap.
let buf =
KTraceBuffer::new(buffer, drop_stats, cpu_ref_header_entry, process_koid, thread_koid);
let boxed_buf = match Box::try_new(buf) {
Ok(b) => b,
Err(_) => return Status::NO_MEMORY.into_raw(),
};
let raw_ptr = Box::into_raw(boxed_buf);
// Store the pointer atomically.
let old_ptr = ktrace.buffers[cpu_num as usize].swap(raw_ptr, Ordering::AcqRel);
if !old_ptr.is_null() {
// If there was a previous buffer, reclaim and drop it.
unsafe {
let _ = Box::from_raw(old_ptr);
}
}
zx_status::sys::ZX_OK
}
/// KTrace tests
#[cfg(all(not(gcc), ktest))]
#[unittest::suite(name = "ktrace_rust")]
mod tests {
use crate::arch_rs::{InterruptDisableGuard, curr_cpu_num, max_num_cpus};
use crate::kernel::thread::{FxtRef, ThreadPtr};
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use core::{ffi, ptr};
use spsc_buffer::Buffer;
use unittest::{
assert_eq, assert_ok, expect_eq, expect_false, expect_ne, expect_true, unwrap_ok,
};
declare_interned_category!(MEMORY_CAT, "kernel:memory", extern);
declare_interned_category!(SCHED_CAT, "kernel:sched", extern);
declare_interned_category!(CONTENTION_CAT, "kernel:contention", extern);
declare_interned_category!(IPC_CAT, "kernel:ipc", extern);
declare_interned_category!(IRQ_CAT, "kernel:irq", extern);
unsafe extern "C" {
fn ktrace_restore_rust_singleton();
}
/// Initialization and size/metadata attributes.
#[test]
fn init_and_size() {
let mut storage = [0u8; 256];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let mut stats = DroppedRecordStats::default();
let kbuf = KTraceBuffer::new(
NonNull::new(ptr::from_mut(&mut inner_buf)).unwrap(),
NonNull::new(ptr::from_mut(&mut stats)).unwrap(),
1,
100,
200,
);
expect_eq!(kbuf.size(), 256);
expect_eq!(kbuf.process_koid, 100);
expect_eq!(kbuf.thread_koid, 200);
}
// Verifies that constructing or operating on buffers with null or invalid storage fails
// gracefully with an error code rather than panicking on slice::from_raw_parts.
//
/// Buffer operations with null/invalid storage fail gracefully without panicking.
#[test]
fn invalid_buffer_guards() {
let _guard = InterruptDisableGuard::new();
// 1. rust_ktrace_init_cpu_buffer with null pointers.
let mut stats = DroppedRecordStats::default();
let stats_ptr = ptr::from_mut(&mut stats).cast::<ffi::c_void>();
let status_null_buf =
unsafe { rust_ktrace_init_cpu_buffer(0, ptr::null_mut(), stats_ptr, 100, 200, 1) };
expect_eq!(status_null_buf, Status::INVALID_ARGS.into_raw());
let mut storage = [0u8; 256];
let mut valid_inner =
unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let valid_buf_ptr = ptr::from_mut(&mut valid_inner).cast::<ffi::c_void>();
let status_null_stats =
unsafe { rust_ktrace_init_cpu_buffer(0, valid_buf_ptr, ptr::null_mut(), 100, 200, 1) };
expect_eq!(status_null_stats, Status::INVALID_ARGS.into_raw());
// 2. rust_ktrace_init_cpu_buffer with invalid backing buffer (null storage / 0 size).
let mut invalid_inner = Buffer::<NoOpAllocator>::empty();
let invalid_buf_ptr = ptr::from_mut(&mut invalid_inner).cast::<ffi::c_void>();
let status_invalid =
unsafe { rust_ktrace_init_cpu_buffer(0, invalid_buf_ptr, stats_ptr, 100, 200, 1) };
expect_eq!(status_invalid, Status::BAD_STATE.into_raw());
// 3. Operating on KTraceBuffer wrapping an empty/invalid buffer with null storage.
let mut kbuf = KTraceBuffer::new(
NonNull::new(ptr::from_mut(&mut invalid_inner)).unwrap(),
NonNull::new(ptr::from_mut(&mut stats)).unwrap(),
1,
100,
200,
);
let header = 4u64 | (2u64 << 4);
expect_true!(kbuf.reserve(header).err() == Some(Status::BAD_STATE));
let read_res = kbuf.read(|_, _| Ok(()), 16);
expect_true!(read_res.err() == Some(Status::BAD_STATE));
}
/// Reservation, writing words, and committing.
#[test]
fn test_write() {
let _guard = InterruptDisableGuard::new();
let mut storage = [0u8; 256];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let mut stats = DroppedRecordStats::default();
let mut kbuf = KTraceBuffer::new(
NonNull::new(ptr::from_mut(&mut inner_buf)).unwrap(),
NonNull::new(ptr::from_mut(&mut stats)).unwrap(),
1,
100,
200,
);
// Reserve 16 bytes (2 words).
let header = 4u64 | (2u64 << 4);
let mut res = unwrap_ok!(kbuf.reserve(header));
// Write a word (8 bytes).
assert_ok!(res.write_word(0xabcdef0123456789));
assert_ok!(res.commit());
// Read it back.
let mut read_bytes = [0u8; 16];
let read_len = unwrap_ok!(kbuf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
16,
));
assert_eq!(read_len, 16);
expect_eq!(u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap()), header);
expect_eq!(u64::from_ne_bytes(read_bytes[8..16].try_into().unwrap()), 0xabcdef0123456789);
}
// Verifies tracking of dropped records and their subsequent serialization when space becomes
// available.
//
/// Dropped record tracking and serialization.
#[test]
fn dropped_record_tracking() {
let _guard = InterruptDisableGuard::new();
let mut storage = [0u8; 128]; // small buffer
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let mut stats = DroppedRecordStats::default();
let mut kbuf = KTraceBuffer::new(
NonNull::new(ptr::from_mut(&mut inner_buf)).unwrap(),
NonNull::new(ptr::from_mut(&mut stats)).unwrap(),
1,
100,
200,
);
// Reserve almost all space.
// 128 bytes total. Let's reserve 96 bytes (12 words).
let header = 4u64 | (12u64 << 4);
let mut res = unwrap_ok!(kbuf.reserve(header));
assert_ok!(res.write_bytes(&[0u8; 88]));
assert_ok!(res.commit());
// Now, try to reserve 64 bytes (8 words). This should fail because there are only 32 bytes
// left.
let header2 = 4u64 | (8u64 << 4);
expect_true!(kbuf.reserve(header2).err() == Some(Status::NO_SPACE));
// This failed reservation should have been tracked!
expect_true!(kbuf.drop_stats().has_dropped());
expect_eq!(kbuf.drop_stats().num_dropped, 1);
expect_eq!(kbuf.drop_stats().bytes_dropped, 64);
// Now, drain the buffer to free all space.
assert_ok!(kbuf.drain());
// Now, reserve 16 bytes (2 words).
// Since first_dropped was set, this reservation should successfully write the 56-byte
// dropped records duration event first!
let header3 = 4u64 | (2u64 << 4);
let mut res3 = unwrap_ok!(kbuf.reserve(header3));
assert_ok!(res3.write_bytes(&[0u8; 8]));
assert_ok!(res3.commit());
// The dropped stats should have been reset!
expect_false!(kbuf.drop_stats().has_dropped());
expect_eq!(kbuf.drop_stats().num_dropped, 0);
expect_eq!(kbuf.drop_stats().bytes_dropped, 0);
// Let's read the buffer content.
let mut read_bytes = [0u8; 72];
let read_len = unwrap_ok!(kbuf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
72,
));
expect_eq!(read_len, 72);
// Verify the DroppedRecordDurationEvent header:
let event_header = u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap());
expect_eq!(event_header & 0xf, 4);
expect_eq!((event_header >> 4) & 0xfff, 7);
expect_eq!((event_header >> 16) & 0xf, 4);
expect_eq!((event_header >> 20) & 0xf, 2);
expect_eq!((event_header >> 24) & 0xff, 1);
expect_eq!((event_header >> 32) & 0xffff, u64::from(META_CAT.label().id()));
expect_eq!((event_header >> 48) & 0xffff, u64::from(DROP_STATS_REF.id()));
// Verify process_koid (100) and thread_koid (200)
expect_eq!(u64::from_ne_bytes(read_bytes[16..24].try_into().unwrap()), 100);
expect_eq!(u64::from_ne_bytes(read_bytes[24..32].try_into().unwrap()), 200);
// Verify the two arguments:
let num_dropped_arg = u64::from_ne_bytes(read_bytes[32..40].try_into().unwrap());
expect_eq!(num_dropped_arg & 0xf, 2);
expect_eq!((num_dropped_arg >> 4) & 0xfff, 1);
expect_eq!((num_dropped_arg >> 16) & 0xffff, u64::from(NUM_RECORDS_REF.id()));
expect_eq!((num_dropped_arg >> 32) & 0xffffffff, 1);
let bytes_dropped_arg = u64::from_ne_bytes(read_bytes[40..48].try_into().unwrap());
expect_eq!(bytes_dropped_arg & 0xf, 2);
expect_eq!((bytes_dropped_arg >> 16) & 0xffff, u64::from(NUM_BYTES_REF.id()));
expect_eq!((bytes_dropped_arg >> 32) & 0xffffffff, 64);
// Verify the reservation header3:
let res_header = u64::from_ne_bytes(read_bytes[56..64].try_into().unwrap());
expect_eq!(res_header, header3);
}
/// Direct invocation of KTraceBuffer::emit_drop_stats.
#[test]
fn emit_drop_stats() {
let _guard = InterruptDisableGuard::new();
let mut storage = [0u8; 128];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let mut stats = DroppedRecordStats::default();
let mut kbuf = KTraceBuffer::new(
NonNull::new(ptr::from_mut(&mut inner_buf)).unwrap(),
NonNull::new(ptr::from_mut(&mut stats)).unwrap(),
1,
100,
200,
);
// 1. Force a failed reservation to track a dropped record.
let header = 4u64 | (32u64 << 4);
expect_true!(kbuf.reserve(header).err() == Some(Status::NO_SPACE));
expect_true!(kbuf.drop_stats().has_dropped());
expect_eq!(kbuf.drop_stats().num_dropped, 1);
expect_eq!(kbuf.drop_stats().bytes_dropped, 256);
// 2. Call emit_drop_stats directly.
assert_ok!(kbuf.emit_drop_stats());
expect_false!(kbuf.drop_stats().has_dropped());
expect_eq!(kbuf.drop_stats().num_dropped, 0);
expect_eq!(kbuf.drop_stats().bytes_dropped, 0);
// 3. Read and verify the event.
let mut read_bytes = [0u8; 56];
let read_len = unwrap_ok!(kbuf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
56,
));
expect_eq!(read_len, 56);
let event_header = u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap());
expect_eq!(event_header & 0xf, 4);
expect_eq!((event_header >> 4) & 0xfff, 7);
expect_eq!((event_header >> 16) & 0xf, 4);
expect_eq!((event_header >> 20) & 0xf, 2);
expect_eq!((event_header >> 24) & 0xff, 1);
expect_eq!((event_header >> 32) & 0xffff, u64::from(META_CAT.label().id()));
expect_eq!((event_header >> 48) & 0xffff, u64::from(DROP_STATS_REF.id()));
expect_eq!(u64::from_ne_bytes(read_bytes[16..24].try_into().unwrap()), 100);
expect_eq!(u64::from_ne_bytes(read_bytes[24..32].try_into().unwrap()), 200);
let num_dropped_arg = u64::from_ne_bytes(read_bytes[32..40].try_into().unwrap());
expect_eq!(num_dropped_arg & 0xf, 2);
expect_eq!((num_dropped_arg >> 16) & 0xffff, u64::from(NUM_RECORDS_REF.id()));
expect_eq!((num_dropped_arg >> 32) & 0xffffffff, 1);
let bytes_dropped_arg = u64::from_ne_bytes(read_bytes[40..48].try_into().unwrap());
expect_eq!(bytes_dropped_arg & 0xf, 2);
expect_eq!((bytes_dropped_arg >> 16) & 0xffff, u64::from(NUM_BYTES_REF.id()));
expect_eq!((bytes_dropped_arg >> 32) & 0xffffffff, 256);
}
// Verifies the full global lifecycle of KTrace: initialization, category bitmasks, CPU buffer
// allocation, and high-level tracing macro execution.
//
/// Validate full global lifecycle.
#[test]
fn global_lifecycle() {
struct RestoreGuard;
impl Drop for RestoreGuard {
fn drop(&mut self) {
unsafe {
ktrace_restore_rust_singleton();
}
}
}
let _restore_guard = RestoreGuard;
let _guard = InterruptDisableGuard::new();
// Initialize indices of the categories we're testing.
META_CAT.set_index(0, kstring::interned_category::InternedCategory::INVALID_INDEX);
MEMORY_CAT.set_index(1, kstring::interned_category::InternedCategory::INVALID_INDEX);
SCHED_CAT.set_index(2, kstring::interned_category::InternedCategory::INVALID_INDEX);
CONTENTION_CAT.set_index(3, kstring::interned_category::InternedCategory::INVALID_INDEX);
IPC_CAT.set_index(4, kstring::interned_category::InternedCategory::INVALID_INDEX);
IRQ_CAT.set_index(5, kstring::interned_category::InternedCategory::INVALID_INDEX);
// 1. Initialize the global KTrace instance with the system CPU count buffers and a local
// mock state.
let num_cpus = max_num_cpus();
let mut local_state = KTraceState {
categories_bitmask: AtomicU32::new(0),
writes_enabled: AtomicBool::new(false),
};
let local_state_ptr = ptr::from_mut(&mut local_state).cast::<ffi::c_void>();
let status = unsafe { rust_ktrace_init(num_cpus, local_state_ptr) };
expect_eq!(status, 0);
let ktrace = KTrace::get_instance();
// 2. Verify initial states.
expect_false!(ktrace.writes_enabled());
expect_eq!(ktrace.categories_bitmask(), 0);
expect_false!(ktrace.is_category_enabled(META_CAT));
expect_false!(ktrace.is_category_enabled(IRQ_CAT));
// 3. Test writes_enabled.
local_state.writes_enabled.store(true, Ordering::Release);
expect_true!(ktrace.writes_enabled());
local_state.writes_enabled.store(false, Ordering::Release);
expect_false!(ktrace.writes_enabled());
// 4. Test categories_bitmask.
let mask = (1 << MEMORY_CAT.index()) | (1 << CONTENTION_CAT.index());
local_state.categories_bitmask.store(mask, Ordering::Release);
expect_eq!(ktrace.categories_bitmask(), mask);
expect_false!(ktrace.is_category_enabled(META_CAT));
expect_true!(ktrace.is_category_enabled(MEMORY_CAT));
expect_false!(ktrace.is_category_enabled(SCHED_CAT));
expect_true!(ktrace.is_category_enabled(CONTENTION_CAT));
expect_false!(ktrace.is_category_enabled(IPC_CAT));
// 5. Test CPU buffer initialization and reserve.
let mut storage = [0u8; 256];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let inner_buf_ptr = ptr::from_mut(&mut inner_buf).cast::<ffi::c_void>();
let mut stats = DroppedRecordStats::default();
let stats_ptr = ptr::from_mut(&mut stats).cast::<ffi::c_void>();
// Initialize current CPU buffer.
let cpu = curr_cpu_num();
let init_status = unsafe {
rust_ktrace_init_cpu_buffer(
cpu, // cpu_num
inner_buf_ptr,
stats_ptr,
100, // process_koid
200, // thread_koid
1, // cpu_ref_header_entry
)
};
expect_eq!(init_status, 0);
// If writes are disabled, reserving should fail.
let header = 4u64 | (2u64 << 4); // Event with 2 words
expect_true!(unsafe { ktrace.reserve(header) }.err() == Some(Status::BAD_STATE));
// Enable writes.
local_state.writes_enabled.store(true, Ordering::Release);
// Now reserve should succeed!
let mut res = unwrap_ok!(unsafe { ktrace.reserve(header) });
assert_ok!(res.write_word(0x1234567890abcdef));
assert_ok!(res.commit());
// Read and verify the written record from the current CPU buffer.
let ptr = ktrace.get_buffer(cpu as usize);
expect_false!(ptr.is_null());
let buf = unsafe { &*ptr };
let mut read_bytes = [0u8; 16];
let read_len = unwrap_ok!(buf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
16,
));
expect_eq!(read_len, 16);
expect_eq!(u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap()), header);
expect_eq!(u64::from_ne_bytes(read_bytes[8..16].try_into().unwrap()), 0x1234567890abcdef);
// 6. Test Rust macros!
let mask = (1 << META_CAT.index()) | (1 << MEMORY_CAT.index());
local_state.categories_bitmask.store(mask, Ordering::Release);
expect_true!(ktrace.is_category_enabled(META_CAT));
// Let's emit an instant event using the macro!
instant!(META_CAT, "my_event", "arg1" => 42i32, "arg2" => "hello");
// Now let's read the record back and verify it!
let mut read_bytes = [0u8; 128];
let read_len = unwrap_ok!(buf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
56,
));
expect_eq!(read_len, 56);
// Verify Header Word
let header_word = u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap());
expect_eq!(header_word & 0xf, 4);
expect_eq!((header_word >> 4) & 0xfff, 7);
expect_eq!((header_word >> 16) & 0xf, 0);
expect_eq!((header_word >> 20) & 0xf, 2);
expect_eq!((header_word >> 32) & 0xffff, u64::from(META_CAT.label().id()));
let name_id = (header_word >> 48) & 0xffff;
expect_ne!(name_id, 0);
// Verify Timestamp (word 1)
let timestamp_word = u64::from_ne_bytes(read_bytes[8..16].try_into().unwrap());
expect_ne!(timestamp_word, 0);
// Verify KOIDs (words 2 & 3)
let process_koid = u64::from_ne_bytes(read_bytes[16..24].try_into().unwrap());
let thread_koid = u64::from_ne_bytes(read_bytes[24..32].try_into().unwrap());
// SAFETY: Tests run after threading has been initialized.
let FxtRef { pid: expected_proc, tid: expected_thread } =
unsafe { ThreadPtr::current().fxt_ref() };
expect_eq!(process_koid, expected_proc);
expect_eq!(thread_koid, expected_thread);
// Verify Argument 1 ("arg1" => 42i32) (word 4)
let arg1_header = u64::from_ne_bytes(read_bytes[32..40].try_into().unwrap());
expect_eq!(arg1_header & 0xf, 1);
expect_eq!((arg1_header >> 4) & 0xfff, 1);
expect_eq!((arg1_header >> 32) & 0xffffffff, 42);
let arg1_name_id = (arg1_header >> 16) & 0xffff;
expect_ne!(arg1_name_id, 0);
// Verify Argument 2 ("arg2" => "hello") (words 5 & 6)
let arg2_header = u64::from_ne_bytes(read_bytes[40..48].try_into().unwrap());
expect_eq!(arg2_header & 0xf, 6);
expect_eq!((arg2_header >> 4) & 0xfff, 2);
expect_eq!((arg2_header >> 32) & 0xffff, 0x8005);
let arg2_name_id = (arg2_header >> 16) & 0xffff;
expect_ne!(arg2_name_id, 0);
let arg2_val = &read_bytes[48..56];
expect_true!(&arg2_val[0..5] == b"hello");
expect_true!(arg2_val[5..8] == [0, 0, 0]);
// 7. Test kernel_object! macro with dynamic inline name and Koid argument.
kernel_object!(META_CAT, 200u64, 1u32, &b"test_proc"[..], "job" => Koid(300u64));
let mut read_bytes = [0u8; 128];
let read_len = unwrap_ok!(buf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
48,
));
expect_eq!(read_len, 48);
// Verify KernelObject Header (word 0): type=7, size=6 words (48 bytes), obj_type=1, name=0x8009, args=1
let ko_header = u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap());
expect_eq!(ko_header & 0xf, 7);
expect_eq!((ko_header >> 4) & 0xfff, 6);
expect_eq!((ko_header >> 16) & 0xff, 1);
expect_eq!((ko_header >> 24) & 0xffff, 0x8009);
expect_eq!((ko_header >> 40) & 0xf, 1);
// Verify KOID (word 1)
expect_eq!(u64::from_ne_bytes(read_bytes[8..16].try_into().unwrap()), 200);
// Verify Inline Name (words 2 & 3: 9 bytes padded to 16 bytes)
expect_true!(&read_bytes[16..25] == b"test_proc");
expect_true!(read_bytes[25..32] == [0; 7]);
// Verify Koid Argument (words 4 & 5: arg header + koid value)
let arg_header = u64::from_ne_bytes(read_bytes[32..40].try_into().unwrap());
expect_eq!(arg_header & 0xf, 8); // ArgumentType::kKoid (8)
expect_eq!((arg_header >> 4) & 0xfff, 2); // ArgumentSize = 2 words
expect_ne!((arg_header >> 16) & 0xffff, 0); // NameStringRef
expect_eq!(u64::from_ne_bytes(read_bytes[40..48].try_into().unwrap()), 300);
}
/// Verifies StringRef encoding and payload calculation for interned and inline strings.
#[test]
fn test_string_ref() {
// Interned string ref
let interned_ref = StringRef::from(DROP_STATS_REF);
expect_eq!(interned_ref.header_entry(), DROP_STATS_REF.id());
expect_eq!(interned_ref.payload_words(), 0);
// Inline string ref from byte slice
let inline_bytes_ref = StringRef::from(&b"foo"[..]);
expect_eq!(inline_bytes_ref.header_entry(), 0x8003);
expect_eq!(inline_bytes_ref.payload_words(), 1);
let inline_longer_ref = StringRef::from(&b"longer_test_string"[..]);
expect_eq!(inline_longer_ref.header_entry(), 0x8012);
expect_eq!(inline_longer_ref.payload_words(), 3);
// Inline string ref from &str
let inline_str_ref = StringRef::from("hello_world");
expect_eq!(inline_str_ref.header_entry(), 0x800b);
expect_eq!(inline_str_ref.payload_words(), 2);
}
/// Verifies Koid argument wrapping and size calculation.
#[test]
fn test_koid_argument() {
let koid_val = Koid(0xfeedface);
let arg_val: ArgValue<'_> = koid_val.into();
match arg_val {
ArgValue::Koid(v) => {
expect_eq!(v, 0xfeedface);
}
_ => panic!("Expected ArgValue::Koid"),
}
let arg = Argument::new(DROP_STATS_REF, koid_val);
expect_eq!(arg.size_words(), 2);
}
/// Verifies argument size and header encoding across all argument types.
#[test]
fn test_all_argument_types() {
let _guard = InterruptDisableGuard::new();
let mut storage = [0u8; 1024];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let mut stats = DroppedRecordStats::default();
let mut kbuf = KTraceBuffer::new(
NonNull::new(ptr::from_mut(&mut inner_buf)).unwrap(),
NonNull::new(ptr::from_mut(&mut stats)).unwrap(),
1,
100,
200,
);
// Test each argument type writing via a reservation.
let args = [
Argument::new(DROP_STATS_REF, ArgValue::Null),
Argument::new(DROP_STATS_REF, true),
Argument::new(DROP_STATS_REF, -123i32),
Argument::new(DROP_STATS_REF, 456u32),
Argument::new(DROP_STATS_REF, -789i64),
Argument::new(DROP_STATS_REF, 101112u64),
Argument::new(DROP_STATS_REF, 123.456f64),
Argument::new(DROP_STATS_REF, "hello_world"),
Argument::new(DROP_STATS_REF, ArgValue::Pointer(0x12345678)),
Argument::new(DROP_STATS_REF, Koid(9999)),
];
let total_arg_words: usize = args.iter().map(|a| a.size_words()).sum();
let total_words = 1 + total_arg_words; // 1 header + args
let header = 4u64 | ((total_words as u64) << 4);
let mut res = unwrap_ok!(kbuf.reserve(header));
for arg in &args {
assert_ok!(arg.write(&mut res));
}
assert_ok!(res.commit());
let mut read_bytes = [0u8; 1024];
let read_len = unwrap_ok!(kbuf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
(total_words * 8) as u32,
));
expect_eq!(read_len, (total_words * 8) as u32);
let mut word_idx = 1; // skip record header
// 0: Null (1 word)
let null_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(null_hdr & 0xf, 0); // kNull
expect_eq!((null_hdr >> 4) & 0xfff, 1);
word_idx += 1;
// 1: Bool (1 word)
let bool_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(bool_hdr & 0xf, 9); // kBool
expect_eq!((bool_hdr >> 4) & 0xfff, 1);
expect_eq!((bool_hdr >> 32) & 1, 1);
word_idx += 1;
// 2: Int32 (1 word)
let i32_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(i32_hdr & 0xf, 1); // kInt32
expect_eq!((i32_hdr >> 4) & 0xfff, 1);
expect_eq!((i32_hdr >> 32) as u32 as i32, -123);
word_idx += 1;
// 3: Uint32 (1 word)
let u32_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(u32_hdr & 0xf, 2); // kUint32
expect_eq!((u32_hdr >> 4) & 0xfff, 1);
expect_eq!((u32_hdr >> 32) as u32, 456);
word_idx += 1;
// 4: Int64 (2 words)
let i64_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(i64_hdr & 0xf, 3); // kInt64
expect_eq!((i64_hdr >> 4) & 0xfff, 2);
let i64_val = i64::from_ne_bytes(
read_bytes[(word_idx + 1) * 8..(word_idx + 2) * 8].try_into().unwrap(),
);
expect_eq!(i64_val, -789);
word_idx += 2;
// 5: Uint64 (2 words)
let u64_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(u64_hdr & 0xf, 4); // kUint64
expect_eq!((u64_hdr >> 4) & 0xfff, 2);
let u64_val = u64::from_ne_bytes(
read_bytes[(word_idx + 1) * 8..(word_idx + 2) * 8].try_into().unwrap(),
);
expect_eq!(u64_val, 101112);
word_idx += 2;
// 6: Double (2 words)
let d_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(d_hdr & 0xf, 5); // kDouble
expect_eq!((d_hdr >> 4) & 0xfff, 2);
let d_bits = u64::from_ne_bytes(
read_bytes[(word_idx + 1) * 8..(word_idx + 2) * 8].try_into().unwrap(),
);
expect_eq!(d_bits, 123.456f64.to_bits());
word_idx += 2;
// 7: String (1 header + 2 payload words = 3 words)
let s_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(s_hdr & 0xf, 6); // kString
expect_eq!((s_hdr >> 4) & 0xfff, 3);
expect_eq!((s_hdr >> 32) & 0xffff, 0x800b); // 11 bytes inline
expect_true!(&read_bytes[(word_idx + 1) * 8..(word_idx + 1) * 8 + 11] == b"hello_world");
word_idx += 3;
// 8: Pointer (2 words)
let ptr_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(ptr_hdr & 0xf, 7); // kPointer
expect_eq!((ptr_hdr >> 4) & 0xfff, 2);
let ptr_val = usize::from_ne_bytes(
read_bytes[(word_idx + 1) * 8..(word_idx + 2) * 8].try_into().unwrap(),
);
expect_eq!(ptr_val, 0x12345678);
word_idx += 2;
// 9: Koid (2 words)
let koid_hdr =
u64::from_ne_bytes(read_bytes[word_idx * 8..(word_idx + 1) * 8].try_into().unwrap());
expect_eq!(koid_hdr & 0xf, 8); // kKoid
expect_eq!((koid_hdr >> 4) & 0xfff, 2);
let koid_val = u64::from_ne_bytes(
read_bytes[(word_idx + 1) * 8..(word_idx + 2) * 8].try_into().unwrap(),
);
expect_eq!(koid_val, 9999);
word_idx += 2;
expect_eq!(word_idx, total_words);
}
/// Verifies bitfield layout construction and encoding.
#[test]
fn test_bitfield_layouts() {
// RecordHeader
let mut rec = RecordHeader::new();
rec.set_record_size(12).set_record_type(RecordType::Event);
expect_eq!(rec.record_type(), RecordType::Event);
expect_eq!(rec.record_size(), 12);
let rec_raw = rec.bits();
expect_eq!(rec_raw & 0xf, 4);
expect_eq!((rec_raw >> 4) & 0xfff, 12);
// LargeRecordHeader
let mut lrec = LargeRecordHeader::default();
lrec.set_record_size(0x1000).set_large_type(2);
expect_eq!(lrec.record_type(), RecordType::LargeBlob);
expect_eq!(lrec.record_size(), 0x1000);
expect_eq!(lrec.large_type(), 2);
let lrec_raw = lrec.bits();
expect_eq!(lrec_raw & 0xf, 15);
expect_eq!((lrec_raw >> 4) & 0xffffffff, 0x1000);
expect_eq!((lrec_raw >> 36) & 0xf, 2);
// StringRefHeader
let mut interned = StringRefHeader::new();
interned.set_id_or_len(42);
expect_eq!(interned.id_or_len(), 42);
expect_false!(interned.is_inline());
expect_eq!(interned.bits(), 42);
let mut inline = StringRefHeader::new();
inline.set_is_inline(true).set_id_or_len(15);
expect_true!(inline.is_inline());
expect_eq!(inline.id_or_len(), 15);
expect_eq!(inline.bits(), 0x800f);
// KernelObjectRecordHeader
let mut ko = KernelObjectRecordHeader::default();
ko.set_record_size(6).set_obj_type(1).set_name_ref(0x8009).set_arg_count(1);
expect_eq!(ko.record_type(), RecordType::KernelObject);
expect_eq!(ko.record_size(), 6);
expect_eq!(ko.obj_type(), 1);
expect_eq!(ko.name_ref(), 0x8009);
expect_eq!(ko.arg_count(), 1);
let ko_raw = ko.bits();
expect_eq!(ko_raw & 0xf, 7);
expect_eq!((ko_raw >> 4) & 0xfff, 6);
expect_eq!((ko_raw >> 16) & 0xff, 1);
expect_eq!((ko_raw >> 24) & 0xffff, 0x8009);
expect_eq!((ko_raw >> 40) & 0xf, 1);
// EventRecordHeader
let mut ev = EventRecordHeader::default();
ev.set_record_size(5)
.set_event_type(EventType::Instant)
.set_arg_count(1)
.set_thread_ref(0)
.set_category_ref(10)
.set_name_ref(20);
expect_eq!(ev.record_type(), RecordType::Event);
expect_eq!(ev.record_size(), 5);
expect_eq!(ev.event_type(), EventType::Instant);
expect_eq!(ev.arg_count(), 1);
expect_eq!(ev.thread_ref(), 0);
expect_eq!(ev.category_ref(), 10);
expect_eq!(ev.name_ref(), 20);
let ev_raw = ev.bits();
expect_eq!(ev_raw & 0xf, 4);
expect_eq!((ev_raw >> 4) & 0xfff, 5);
expect_eq!((ev_raw >> 16) & 0xf, 0);
expect_eq!((ev_raw >> 20) & 0xf, 1);
expect_eq!((ev_raw >> 24) & 0xff, 0);
expect_eq!((ev_raw >> 32) & 0xffff, 10);
expect_eq!((ev_raw >> 48) & 0xffff, 20);
}
}
//
// Zircon C++ kernel-tests FFI tests.
//
/// Test-only FFI helper to write a single word record from Rust.
///
/// # Safety
///
/// This must be called with interrupts disabled.
#[cfg(ktest)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ktrace_test_interop(header: u64, val: u64) -> i32 {
let ktrace = KTrace::get_instance();
// SAFETY: The caller guarantees interrupts are disabled.
if let Ok(mut res) = unsafe { ktrace.reserve(header) } {
let _ = res.write_word(val);
let _ = res.commit();
0
} else {
-1
}
}
/// Test-only FFI helper to exercise all ktrace macros from Rust.
///
/// # Safety
///
/// This must be called with interrupts disabled and KTrace active.
#[cfg(ktest)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ktrace_test_macros() {
// Exercise each macro. We'll write specific, distinguishable events.
instant!("kernel:meta", "rust_instant", Context::Thread, "val" => 101u32);
duration_begin!("kernel:meta", "rust_duration", Context::Thread, "val" => 102u32);
duration_end!("kernel:meta", "rust_duration", Context::Thread, "val" => 103u32);
counter!("kernel:meta", "rust_counter", 104u64, "val" => 105u32);
flow_begin!("kernel:meta", "rust_flow", 106u64, "val" => 107u32);
flow_step!("kernel:meta", "rust_flow", 106u64, "val" => 108u32);
flow_end!("kernel:meta", "rust_flow", 106u64, "val" => 109u32);
complete!("kernel:meta", "rust_complete", InstantBootTicks(110i64), "val" => 111u32);
kernel_object!("kernel:meta", 112u64, 1u32, "rust_kernel_obj", "val" => 113u32);
kernel_object_always!(114u64, 2u32, "rust_kernel_obj_always", "val" => 115u32);
kernel_object!("kernel:meta", 116u64, 1u32, &b"dynamic_proc"[..], "job" => Koid(117u64));
}