blob: f17cae6805c98bf300a6750afe79f00301f968b0 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use fidl_next::fuchsia::channel::Buffer;
use fidl_next::wire::Uint64;
use fidl_next::{AsDecoderExt as _, CHUNK_SIZE, Message, Method, Request};
use fidl_next_fuchsia_boot::svc_stash::Store;
use fidl_next_fuchsia_boot::userboot::{
PostBootfsFiles, PostBootfsVmo, PostBootHandles, PostStashSvc,
};
use fidl_next_fuchsia_debugdata::publisher::Publish;
use fuchsia_runtime::{HandleInfo, HandleType, take_startup_handle};
use std::fmt::Write as _;
use std::panic::set_hook;
use std::process::abort;
use zx::sys::{ZX_HANDLE_INVALID, zx_handle_t};
use zx::{
Channel, EventPair, HandleRef, Koid, MonotonicInstant, NullableHandle, ObjectType, Signals,
Vmo, WaitResult,
};
use zx_libc::sanitizer::Log;
pub use fidl_next_fuchsia_boot::natural::BootfsFileVmo;
unsafe extern "C" {
fn dl_set_loader_service(new_svc: zx_handle_t) -> zx_handle_t;
}
/// Decoded contents of a `fuchsia.debugdata.Publisher/Publish` FIDL request.
#[derive(Debug)]
pub struct DebugDataMessage {
pub sink: String,
pub vmo: Vmo,
pub token: EventPair,
}
/// Installs a panic hook that writes panic messages to the kernel debuglog via
/// `__sanitizer_log_write` before aborting.
pub fn init_test_logging() {
set_hook(Box::new(|info| {
let _ = writeln!(&mut Log::new(), "{info}");
abort();
}));
}
/// Takes the `/svc` startup handle (`PA_HND(PA_NS_DIR, 0)`).
///
/// Callers must keep the returned [`Channel`] alive for as long as
/// `zx_libc::sanitizer::publish_data` is called, because libc's preinit stores
/// an unowned borrow of this handle in `__zircon_namespace_svc`.
pub fn take_svc_channel() -> Channel {
let handle = take_startup_handle(HandleInfo::new(HandleType::NamespaceDirectory, 0))
.expect("missing /svc startup handle");
assert!(!handle.is_invalid());
Channel::from(handle)
}
/// Returns the kernel object ID of `handle`.
pub fn koid(handle: HandleRef<'_>) -> Koid {
handle.koid().expect("failed to get koid")
}
/// Returns the peer kernel object ID (`related_koid`) of `handle`.
pub fn peer_koid(handle: HandleRef<'_>) -> Koid {
handle.basic_info().expect("failed to get basic_info").related_koid
}
fn is_channel_readable(channel: &Channel) -> bool {
match channel.wait_one(Signals::CHANNEL_READABLE, MonotonicInstant::INFINITE_PAST) {
WaitResult::Ok(observed) => observed.contains(Signals::CHANNEL_READABLE),
WaitResult::TimedOut(_) => false,
other => panic!("unexpected wait_one result on channel: {other:?}"),
}
}
fn read_fidl_message(channel: &Channel) -> Message<Channel> {
let mut bytes = Vec::new();
let mut handles = Vec::new();
channel.read_split(&mut bytes, &mut handles).expect("failed to read FIDL message from channel");
assert_eq!(bytes.len() % CHUNK_SIZE, 0, "FIDL message is not 8-byte aligned");
let chunks = bytes
.chunks_exact(CHUNK_SIZE)
.map(|chunk| {
Uint64(u64::from_le_bytes(chunk.try_into().expect("chunk has CHUNK_SIZE bytes")))
})
.collect();
let buffer = Buffer { chunks, handles };
Message::<Channel>::decode(buffer).expect("failed to decode FIDL header")
}
fn drain_userboot(userboot: Channel) -> (Option<Channel>, Option<Vmo>, Vec<BootfsFileVmo>, bool) {
let mut stash_svc = None;
let mut bootfs_vmo = None;
let mut entries = Vec::new();
let mut got_boot_handles = false;
while is_channel_readable(&userboot) {
let mut message = read_fidl_message(&userboot);
let ordinal = *message.header().ordinal;
if ordinal == PostStashSvc::ORDINAL {
let decoded = message
.into_decoded::<<PostStashSvc as Method>::Request>()
.expect("failed to decode PostStashSvc request");
let payload = Request::<PostStashSvc, Channel>::from_decoded(decoded).payload();
stash_svc = Some(payload.stash_svc_endpoint.into_untyped());
} else if ordinal == PostBootfsVmo::ORDINAL {
let decoded = message
.into_decoded::<<PostBootfsVmo as Method>::Request>()
.expect("failed to decode PostBootfsVmo request");
let payload = Request::<PostBootfsVmo, Channel>::from_decoded(decoded).payload();
bootfs_vmo = Some(payload.bootfs);
} else if ordinal == PostBootfsFiles::ORDINAL {
let decoded = message
.into_decoded::<<PostBootfsFiles as Method>::Request>()
.expect("failed to decode PostBootfsFiles request");
let payload = Request::<PostBootfsFiles, Channel>::from_decoded(decoded).payload();
entries.extend(payload.files);
} else if ordinal == PostBootHandles::ORDINAL {
let decoded = message
.into_decoded::<<PostBootHandles as Method>::Request>()
.expect("failed to decode PostBootHandles request");
let payload = Request::<PostBootHandles, Channel>::from_decoded(decoded).payload();
assert!(!payload.zbi_vmo.expect("missing zbi_vmo in PostBootHandles").is_invalid());
got_boot_handles = true;
} else {
panic!("unexpected fuchsia.boot.Userboot ordinal: {ordinal}");
}
}
(stash_svc, bootfs_vmo, entries, got_boot_handles)
}
/// Extracts the `fuchsia.boot.SvcStash` server channel from the startup
/// `fuchsia.boot.Userboot` channel (`PA_HND(PA_USER0, 0)`).
///
/// Mutually exclusive with [`bootfs_entries`].
pub fn svc_stash() -> Channel {
let userboot: Channel = take_startup_handle(HandleInfo::new(HandleType::User0, 0))
.expect("missing PA_USER0 startup handle")
.into();
assert!(!userboot.is_invalid());
let (stash_svc, _bootfs_vmo, _entries, _got_boot_handles) = drain_userboot(userboot);
let stash_svc = stash_svc.expect("Userboot did not post a SvcStash endpoint");
assert!(!stash_svc.is_invalid());
stash_svc
}
/// Extracts the `BootfsFileVmo` entries posted to the startup
/// `fuchsia.boot.Userboot` channel (`PA_HND(PA_USER0, 0)`).
///
/// Mutually exclusive with [`svc_stash`].
pub fn bootfs_entries() -> Vec<BootfsFileVmo> {
let userboot: Channel = take_startup_handle(HandleInfo::new(HandleType::User0, 0))
.expect("missing PA_USER0 startup handle")
.into();
assert!(!userboot.is_invalid());
// Close the loader service channel so userboot finishes serving loader
// requests, posts the bootfs entries, and closes its Userboot endpoint.
// SAFETY: `dl_set_loader_service(ZX_HANDLE_INVALID)` replaces the active
// loader service handle with `ZX_HANDLE_INVALID` and returns ownership of
// the previous handle.
let old_loader = unsafe { NullableHandle::from_raw(dl_set_loader_service(ZX_HANDLE_INVALID)) };
drop(old_loader);
let wait_res = userboot.wait_one(Signals::CHANNEL_PEER_CLOSED, MonotonicInstant::INFINITE);
match wait_res {
WaitResult::Ok(signals) => assert!(signals.contains(Signals::CHANNEL_PEER_CLOSED)),
other => panic!("unexpected wait_one result waiting for userboot peer close: {other:?}"),
}
let (_stash_svc, bootfs_vmo, entries, got_boot_handles) = drain_userboot(userboot);
assert!(bootfs_vmo.is_some(), "Userboot did not post a BootfsVmo");
assert!(got_boot_handles, "Userboot did not post BootHandles");
for htype in [
HandleType::BootfsVmo,
HandleType::BootdataVmo,
HandleType::SystemResource,
HandleType::MmioResource,
HandleType::IrqResource,
HandleType::IoportResource,
HandleType::SmcResource,
HandleType::KernelFileVmo,
] {
assert!(
take_startup_handle(HandleInfo::new(htype, 0)).is_none(),
"{htype:?} should not be passed in startup handles when Userboot protocol is used"
);
}
assert!(!entries.is_empty());
entries
}
/// Reads stashed `/svc` server endpoints from `svc_stash`.
///
/// Returns `(svc_0, svc_1)`, where `svc_1` is the most recently stored `/svc`
/// endpoint and `svc_0` (if present) is the previously stored `/svc` endpoint.
pub fn stashed_svc(svc_stash: Channel) -> (Option<Channel>, Channel) {
assert!(!svc_stash.is_invalid());
let mut svcs = Vec::new();
while is_channel_readable(&svc_stash) {
let mut message = read_fidl_message(&svc_stash);
assert_eq!(
*message.header().ordinal,
Store::ORDINAL,
"unexpected fuchsia.boot.SvcStash ordinal"
);
let decoded = message
.into_decoded::<<Store as Method>::Request>()
.expect("failed to decode SvcStash.Store request");
let payload = Request::<Store, Channel>::from_decoded(decoded).payload();
svcs.push(payload.svc_endpoint.into_untyped());
}
assert!(!svcs.is_empty(), "no stashed svc endpoints found");
let svc_1 = svcs.pop().expect("svcs is non-empty");
assert!(!svc_1.is_invalid());
let svc_0 = svcs.pop();
if let Some(ref svc_0) = svc_0 {
assert!(!svc_0.is_invalid());
}
assert!(svcs.is_empty(), "expected at most 2 stashed svc endpoints");
(svc_0, svc_1)
}
/// Reads the most recently stashed `/svc` server endpoint from `svc_stash`.
pub fn single_stashed_svc(svc_stash: Channel) -> Channel {
let (_dropped, svc) = stashed_svc(svc_stash);
svc
}
/// Reads a pipelined `fuchsia.debugdata.Publisher/Publish` message from `svc`.
pub fn debug_data_message(svc: &Channel) -> DebugDataMessage {
match svc.wait_one(Signals::CHANNEL_READABLE, MonotonicInstant::INFINITE_PAST) {
WaitResult::Ok(observed) => assert!(observed.contains(Signals::CHANNEL_READABLE)),
other => panic!("expected svc channel to be readable, got: {other:?}"),
}
// Read the directory open request carrying the server end of the Publisher protocol.
let mut open_bytes = Vec::new();
let mut open_handles = Vec::new();
svc.read_split(&mut open_bytes, &mut open_handles)
.expect("failed to read open request from svc channel");
assert!(!open_bytes.is_empty());
assert_eq!(open_handles.len(), 1);
let debug_data_handle = open_handles.pop().expect("verified 1 handle");
let info = debug_data_handle.basic_info().expect("failed to get basic_info on handle");
assert_eq!(info.object_type, ObjectType::CHANNEL);
let debug_data = Channel::from(debug_data_handle);
match debug_data.wait_one(Signals::CHANNEL_READABLE, MonotonicInstant::INFINITE_PAST) {
WaitResult::Ok(observed) => assert!(observed.contains(Signals::CHANNEL_READABLE)),
other => panic!("expected debug_data channel to be readable, got: {other:?}"),
}
let mut message = read_fidl_message(&debug_data);
assert_eq!(
*message.header().ordinal,
Publish::ORDINAL,
"unexpected fuchsia.debugdata.Publisher ordinal"
);
let decoded = message
.into_decoded::<<Publish as Method>::Request>()
.expect("failed to decode Publisher.Publish request");
let payload = Request::<Publish, Channel>::from_decoded(decoded).payload();
assert!(!payload.data.is_invalid());
assert!(!payload.vmo_token.is_invalid());
DebugDataMessage { sink: payload.data_sink, vmo: payload.data, token: payload.vmo_token }
}
/// Calls `callable` on `channel` while `channel` has readable messages and
/// `callable` returns `true` to keep going.
pub fn on_each_message(channel: &Channel, mut callable: impl FnMut(&Channel) -> bool) {
while is_channel_readable(channel) {
if !callable(channel) {
return;
}
}
}