mod access;
mod access_policy;
pub mod bpf;
mod control;
mod cpu_id;
#[cfg(target_arch = "aarch64")]
mod cpu_worker;
#[cfg(target_arch = "aarch64")]
mod event_map;
pub mod hw;
#[cfg(target_arch = "aarch64")]
mod hw_allocation;
mod hw_event;
#[cfg(target_arch = "aarch64")]
mod hw_open;
#[cfg(target_arch = "aarch64")]
mod hw_owner;
#[cfg(target_arch = "aarch64")]
mod hw_sampling;
#[cfg(target_arch = "aarch64")]
mod inheritance;
#[cfg(target_arch = "aarch64")]
mod inheritance_lifecycle;
pub mod kprobe;
#[cfg(target_arch = "aarch64")]
mod output;
pub mod raw_tracepoint;
#[cfg(target_arch = "aarch64")]
mod rdpmc;
#[cfg(target_arch = "aarch64")]
mod resource_lifecycle;
#[cfg(target_arch = "aarch64")]
pub mod sampling;
#[cfg(target_arch = "aarch64")]
mod sampling_lifecycle;
#[cfg(target_arch = "aarch64")]
mod sampling_registry;
#[cfg(target_arch = "aarch64")]
pub mod sideband;
mod target;
#[cfg(target_arch = "aarch64")]
pub mod task;
#[cfg(target_arch = "aarch64")]
pub(crate) mod task_context;
#[cfg(target_arch = "aarch64")]
mod task_context_state;
#[cfg(target_arch = "aarch64")]
mod task_sideband;
pub mod tracepoint;
pub mod uprobe;
use alloc::{borrow::Cow, boxed::Box, sync::Arc, vec};
use core::{
any::Any,
ffi::c_void,
fmt::Debug,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
};
use ax_io::{Read, Write};
use ax_lazyinit::LazyInit;
use ax_memory_addr::{PAGE_SIZE_4K, PhysAddr, PhysAddrRange, VirtAddr};
use ax_runtime::hal::{paging::MappingFlags, pmu};
use axpoll::{ExclusiveRegistrationSink, Pollable, SharedRegistrationSink};
pub use bpf::BpfPerfEventWrapper;
use hashbrown::HashMap;
use kbpf_basic::{
linux_bpf::perf_event_attr,
perf::{PerfEventIoc, PerfProbeArgs, PerfTypeId},
};
#[cfg(target_arch = "aarch64")]
use self::output::validate_output_redirect;
use self::{
access::ResolvedPerfTarget,
control::PerfControl,
target::{PerfOpenFlags, PerfTarget, PerfTargetError},
};
use crate::{
StarryError, StarryResult,
ebpf::{error::BpfResultExt, transform::EbpfKernelAuxiliary},
file::{FileLike, Kstat, add_file_like, get_file_like},
mm::{VmBytes, VmBytesMut},
pseudofs::DeviceMmap,
sync::{IrqMutex, Mutex},
};
static NEXT_PERF_EVENT_ID: AtomicU64 = AtomicU64::new(1);
pub fn read_midr_el1() -> u64 {
pmu::cpu_id_raw().unwrap_or(0)
}
const PERF_IOC_TYPE: u32 = 0x24;
const PERF_IOC_NR_SET_OUTPUT: u32 = 5;
const PERF_IOC_NR_ID: u32 = 7;
pub trait PerfEventOps: Pollable + Send + Sync + Debug {
fn finish_open(&mut self) -> crate::StarryResult<()> {
Ok(())
}
fn enable(&mut self) -> StarryResult<()>;
fn disable(&mut self) -> StarryResult<()>;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_bpf_prog(&mut self, _bpf_prog: Arc<dyn FileLike>) -> StarryResult<()> {
Err(StarryError::Unsupported)
}
fn device_mmap(&mut self, _len: usize) -> StarryResult<(PhysAddr, Arc<dyn Any + Send + Sync>)> {
Err(StarryError::Unsupported)
}
fn read_values(&mut self) -> StarryResult<PerfReadValues> {
Err(StarryError::Unsupported)
}
fn reset(&mut self) -> StarryResult<()> {
Err(StarryError::Unsupported)
}
fn set_sample_id(&mut self, _id: u64) {}
}
const PERF_FORMAT_TOTAL_TIME_ENABLED: u64 = 1 << 0;
const PERF_FORMAT_TOTAL_TIME_RUNNING: u64 = 1 << 1;
const PERF_FORMAT_ID: u64 = 1 << 2;
pub struct PerfReadValues {
pub value: u64,
pub time_enabled: u64,
pub time_running: u64,
pub read_format: u64,
}
pub struct PerfEvent {
event: Mutex<Box<dyn PerfEventOps>>,
control: Option<Arc<dyn PerfControl>>,
irq_output: Option<bpf::BpfPerfOutput>,
bpf_poll: Option<bpf::BpfPerfPoll>,
id: u64,
nonblocking: AtomicBool,
}
impl Debug for PerfEvent {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PerfEvent").field("id", &self.id).finish()
}
}
impl PerfEvent {
pub fn new(mut event: Box<dyn PerfEventOps>) -> crate::StarryResult<Self> {
let id = NEXT_PERF_EVENT_ID.fetch_add(1, Ordering::Relaxed);
event.set_sample_id(id);
event.finish_open()?;
#[cfg(target_arch = "aarch64")]
let control = event
.as_any_mut()
.downcast_mut::<hw::HwPerfEvent>()
.map(|event| event.control_handle());
#[cfg(not(target_arch = "aarch64"))]
let control = None;
let irq_output = event
.as_any_mut()
.downcast_mut::<BpfPerfEventWrapper>()
.map(|event| event.output_handle());
let bpf_poll = event
.as_any_mut()
.downcast_mut::<BpfPerfEventWrapper>()
.map(|event| event.poll_handle());
Ok(PerfEvent {
event: Mutex::new(event),
control,
irq_output,
bpf_poll,
id,
nonblocking: AtomicBool::new(false),
})
}
fn set_output(&self, arg: usize) -> crate::StarryResult<usize> {
if arg as i32 == -1 {
#[cfg(target_arch = "aarch64")]
if let Some(control) = &self.control {
control.detach_output()?;
}
return Ok(0);
}
let target = get_file_like(arg as i32)?;
let target = target
.into_any_arc()
.downcast::<PerfEvent>()
.map_err(|_| crate::StarryError::InvalidInput)?;
if target.id == self.id {
return Err(crate::StarryError::InvalidInput);
}
#[cfg(not(target_arch = "aarch64"))]
return Err(crate::StarryError::InvalidInput);
#[cfg(target_arch = "aarch64")]
{
let target_control = target
.control
.as_ref()
.ok_or(crate::StarryError::InvalidInput)?;
let target_scope = target_control
.output_scope()
.ok_or(crate::StarryError::InvalidInput)?;
let output = target_control
.output_ring()
.ok_or(crate::StarryError::InvalidInput)?;
if let Some(control) = &self.control {
let source_scope = control
.output_scope()
.ok_or(crate::StarryError::InvalidInput)?;
validate_output_redirect(self.id, target.id, source_scope, target_scope)
.map_err(|_| crate::StarryError::InvalidInput)?;
control.redirect_output(output)?;
}
Ok(0)
}
}
}
impl Pollable for PerfEvent {
fn poll(&self) -> axpoll::IoEvents {
if let Some(control) = &self.control {
control.poll()
} else if let Some(poll) = &self.bpf_poll {
poll.poll()
} else {
self.event.lock().poll()
}
}
unsafe fn register_shared(
&self,
sink: &mut dyn SharedRegistrationSink,
events: axpoll::IoEvents,
) {
if let Some(control) = &self.control {
unsafe { control.register_shared(sink, events) };
} else if let Some(poll) = &self.bpf_poll {
unsafe { poll.register_shared(sink, events) };
}
}
unsafe fn register_exclusive(
&self,
sink: &mut dyn ExclusiveRegistrationSink,
events: axpoll::IoEvents,
) {
if let Some(control) = &self.control {
unsafe { control.register_exclusive(sink, events) };
} else if let Some(poll) = &self.bpf_poll {
unsafe { poll.register_exclusive(sink, events) };
}
}
}
impl FileLike for PerfEvent {
fn read(&self, dst: &mut crate::file::IoDst) -> StarryResult<usize> {
let values = if let Some(control) = &self.control {
control.read_values()?
} else {
self.event.lock().read_values()?
};
let mut fields = [0u64; 4];
let mut n = 0;
fields[n] = values.value;
n += 1;
if values.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED != 0 {
fields[n] = values.time_enabled;
n += 1;
}
if values.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING != 0 {
fields[n] = values.time_running;
n += 1;
}
if values.read_format & PERF_FORMAT_ID != 0 {
fields[n] = self.id;
n += 1;
}
let total = n * core::mem::size_of::<u64>();
if dst.remaining_mut() < total {
return Err(StarryError::InvalidInput);
}
for value in &fields[..n] {
dst.write(&value.to_ne_bytes())?;
}
Ok(total)
}
fn write(&self, _src: &mut crate::file::IoSrc) -> StarryResult<usize> {
Err(StarryError::Unsupported)
}
fn stat(&self) -> StarryResult<Kstat> {
Ok(Kstat::default())
}
fn path(&self) -> Cow<'_, str> {
"anon_inode:[perf_event]".into()
}
fn ioctl(
&self,
current: &crate::task::UserTaskRef,
cmd: u32,
arg: usize,
) -> crate::StarryResult<usize> {
if (cmd >> 8) & 0xff == PERF_IOC_TYPE {
match cmd & 0xff {
PERF_IOC_NR_ID => {
VmBytesMut::new(current, arg as *mut u8, core::mem::size_of::<u64>())
.write(&self.id.to_ne_bytes())?;
return Ok(0);
}
PERF_IOC_NR_SET_OUTPUT => {
return self.set_output(arg);
}
_ => {}
}
}
const PERF_EVENT_IOC_RESET: u32 = 0x2403;
if cmd == PERF_EVENT_IOC_RESET {
if let Some(control) = &self.control {
control.reset()?;
} else {
self.event.lock().reset()?;
}
return Ok(0);
}
let req = PerfEventIoc::try_from(cmd).map_err(|_| StarryError::InvalidInput)?;
match req {
PerfEventIoc::Enable => {
if let Some(control) = &self.control {
control.enable()?;
} else {
self.event.lock().enable()?;
}
}
PerfEventIoc::Disable => {
if let Some(control) = &self.control {
control.disable()?;
} else {
self.event.lock().disable()?;
}
}
PerfEventIoc::SetBpf => {
let bpf_prog_fd = arg as i32;
let file = get_file_like(bpf_prog_fd)?;
self.event.lock().set_bpf_prog(file)?;
}
}
Ok(0)
}
fn device_mmap(&self, offset: u64, length: u64) -> StarryResult<DeviceMmap> {
if offset != 0 {
return Err(StarryError::InvalidInput);
}
let len = length as usize;
let (paddr, anchor) = if let Some(control) = &self.control {
control.device_mmap(len)?
} else {
self.event.lock().device_mmap(len)?
};
Ok(DeviceMmap::Physical(
PhysAddrRange::from_start_size(paddr, len),
Some(anchor),
))
}
fn nonblocking(&self) -> bool {
self.nonblocking.load(Ordering::Acquire)
}
fn set_nonblocking(&self, on: bool) -> StarryResult {
self.nonblocking.store(on, Ordering::Release);
Ok(())
}
}
pub fn sys_perf_event_open(
current: &crate::task::UserTaskRef,
attr_uptr: usize,
pid: i32,
cpu: i32,
group_fd: i32,
flags: u64,
) -> StarryResult<isize> {
let mut buf = vec![0u8; core::mem::size_of::<perf_event_attr>()];
VmBytes::new(current, attr_uptr as *mut u8, buf.len()).read(&mut buf)?;
let attr = unsafe { &*(buf.as_ptr() as *const perf_event_attr) };
perf_event_open(attr, pid, cpu, group_fd, flags)
}
pub fn perf_event_open(
attr: &perf_event_attr,
pid: i32,
cpu: i32,
group_fd: i32,
flags: u64,
) -> crate::StarryResult<isize> {
let flags = PerfOpenFlags::parse(flags).map_err(|_| crate::StarryError::InvalidInput)?;
if flags.contains(PerfOpenFlags::PID_CGROUP) {
return Err(crate::StarryError::Unsupported);
}
let target = PerfTarget::parse(pid, cpu).map_err(|error| match error {
PerfTargetError::InvalidTuple => crate::StarryError::InvalidInput,
PerfTargetError::NoSuchProcess => crate::StarryError::NoSuchProcess,
})?;
let target = ResolvedPerfTarget::resolve(target, ax_runtime::hal::cpu_num())?;
if attr.sigtrap() != 0 {
return Err(crate::StarryError::Unsupported);
}
let is_hardware = attr.type_ == PerfTypeId::PERF_TYPE_HARDWARE as u32
|| attr.type_ == PerfTypeId::PERF_TYPE_RAW as u32
|| attr.type_ == hw::ARMV8_PMUV3_PERF_TYPE;
let validated_hw = is_hardware
.then(|| hw::validate_perf_event_open_hw(attr, target.kind()))
.transpose()?;
let probe_args = if is_hardware {
None
} else {
Some(
PerfProbeArgs::try_from_perf_attr::<EbpfKernelAuxiliary>(
attr,
pid,
cpu,
group_fd,
flags.bits(),
)
.into_starry_result()?,
)
};
target.with_authorized(attr.sigtrap() != 0, |target| {
let event: Box<dyn PerfEventOps> = if is_hardware {
Box::new(hw::perf_event_open_hw(
attr,
target,
validated_hw.expect("hardware perf open has validated attributes"),
)?)
} else {
let args = probe_args.expect("non-hardware perf open has validated probe arguments");
match args.type_ {
PerfTypeId::PERF_TYPE_KPROBE => Box::new(kprobe::perf_event_open_kprobe(args)?),
PerfTypeId::PERF_TYPE_SOFTWARE => Box::new(bpf::perf_event_open_bpf(args)),
PerfTypeId::PERF_TYPE_TRACEPOINT => {
Box::new(tracepoint::perf_event_open_tracepoint(args)?)
}
PerfTypeId::PERF_TYPE_UPROBE => {
Box::new(uprobe::perf_event_open_uprobe(args, target.task())?)
}
_ => {
warn!("perf_event_open: unsupported type {:?}", args.type_);
return Err(crate::StarryError::Unsupported);
}
}
};
let event_arc: Arc<dyn FileLike> = Arc::new(PerfEvent::new(event)?);
let cloexec = flags.contains(PerfOpenFlags::FD_CLOEXEC);
let fd = add_file_like(event_arc.clone(), cloexec)?;
PERF_FILE
.get()
.expect("perf subsystem not initialized")
.lock()
.insert(fd as usize, Arc::downgrade(&event_arc));
Ok(fd as isize)
})
}
static PERF_FILE: LazyInit<IrqMutex<HashMap<usize, alloc::sync::Weak<dyn FileLike>>>> =
LazyInit::new();
pub fn perf_event_init() {
PERF_FILE.init_once(IrqMutex::new(HashMap::new()));
#[cfg(target_arch = "aarch64")]
cpu_worker::init();
}
pub fn perf_event_output(
_ctx: *mut c_void,
fd: usize,
_flags: u32,
data: &[u8],
) -> StarryResult<()> {
let table = PERF_FILE.get().ok_or(StarryError::NotFound)?;
let mut map = table.lock();
let weak = map.get(&fd).ok_or(StarryError::NotFound)?;
let Some(file) = weak.upgrade() else {
map.remove(&fd);
return Err(StarryError::NotFound);
};
drop(map);
let perf_event = file
.into_any_arc()
.downcast::<PerfEvent>()
.map_err(|_| StarryError::InvalidInput)?;
perf_event
.irq_output
.as_ref()
.ok_or(StarryError::InvalidInput)?
.write_event(data)
}
#[cfg(all(test, axtest))]
fn control_callback_runs_preemptible_for_test() -> bool {
#[derive(Debug)]
struct YieldingControl {
preemptible: Arc<AtomicBool>,
}
impl Pollable for YieldingControl {
fn poll(&self) -> axpoll::IoEvents {
axpoll::IoEvents::empty()
}
unsafe fn register_shared(
&self,
_sink: &mut dyn axpoll::SharedRegistrationSink,
_events: axpoll::IoEvents,
) {
}
}
impl PerfEventOps for YieldingControl {
fn enable(&mut self) -> crate::StarryResult<()> {
self.preemptible.store(
ax_runtime::task::thread::current::yield_current_cpu().is_ok(),
Ordering::Release,
);
Ok(())
}
fn disable(&mut self) -> StarryResult<()> {
Ok(())
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
let preemptible = Arc::new(AtomicBool::new(false));
let event = PerfEvent::new(Box::new(YieldingControl {
preemptible: Arc::clone(&preemptible),
}))
.expect("failed to create perf control test event");
event
.event
.lock()
.enable()
.expect("failed to enable perf control test event");
preemptible.load(Ordering::Acquire)
}
#[allow(unused)]
struct BPFJitMemory {
num_pages: usize,
pages: VirtAddr,
}
#[allow(unused)]
impl BPFJitMemory {
fn new(num_pages: usize) -> StarryResult<Self> {
let hint = ax_runtime::hal::mem::virtual_address_space()
.expect("kernel virtual address layout is initialized")
.kernel()
.start;
let virt_start = ax_runtime::kernel_mapping::allocate_kernel_range(
hint,
num_pages * PAGE_SIZE_4K,
MappingFlags::READ | MappingFlags::WRITE | MappingFlags::EXECUTE,
true,
)?;
Ok(BPFJitMemory {
num_pages,
pages: virt_start,
})
}
unsafe fn as_static_mut_slice(&mut self) -> &'static mut [u8] {
unsafe {
core::slice::from_raw_parts_mut(
self.pages.as_ptr() as *mut u8,
self.num_pages * PAGE_SIZE_4K,
)
}
}
}
impl Drop for BPFJitMemory {
fn drop(&mut self) {
ax_runtime::kernel_mapping::unmap_kernel_range(self.pages, self.num_pages * PAGE_SIZE_4K)
.expect("failed to unmap BPF JIT memory");
}
}
#[cfg(all(test, axtest))]
mod tests {
#[cfg(all(test, axtest))]
#[axtest::axtest]
fn control_callback_runs_preemptible() {
assert!(super::control_callback_runs_preemptible_for_test());
}
}