Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/arch-platform-porting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Current Axvisor LoongArch QEMU bring-up uses the dynamic UEFI platform path. The
- **Runtime CPU limits**: treat generated `CPU_CAPACITY`/`SMP` as a build-time capacity for const generics, per-CPU arrays, and linker/percpu layout only. Actual online/usable CPU count must flow through `ax_hal::cpu_num()`, which caps the platform-discovered count by capacity.
- **IRQ namespace rules**: keep CPU trap vectors, platform `IrqId { domain, hwirq }`, firmware sources (`IrqSource::AcpiGsi`, `IrqSource::AcpiGsiRoute`, explicit `IrqSource::ControllerLine`, and driver binding metadata such as `BindingIrqSource::FdtInterrupt`), controller-local hardware lines (`HwIrq`), and guest GSI/vector values in separate namespaces. New runtime IRQ registrations must use `IrqId`, not `usize`; legacy `IrqNumber(raw)` is only for static or still-unmigrated platform boundaries and must live in OS/HAL-facing layers such as `ax-plat`, `ax-hal`, or `axklib`, not `irq-framework` or `somehal`. `irq-framework` owns generic registry, affinity, execution, and boxed callback dispatch semantics; platform rebase work must preserve `BoxedIrqHandler`, `IrqExecution`, and `IrqRequest::new_boxed` while adapting the surrounding platform code to `IrqId`. `LEGACY_IRQ_DOMAIN` and `CPU_LOCAL_IRQ_DOMAIN` remain fixed compatibility domains, while dynamic `somehal` external controller domains such as GIC, PLIC, IOAPIC, EIOINTC, and PCH-PIC are allocated at controller probe time and must be reached through `alloc_irq_domain`, `domain_by_kind`, `domain_by_owner`, or `domain_is_kind`, not by constructing fixed numeric controller domains in dynamic-platform code. Do not derive a host IRQ with arithmetic such as `0x20 + gsi`, `PCI_INTX_VECTOR_BASE + gsi`, or by subtracting a trap-vector base in Axvisor/device code. Resolve firmware/device descriptions with `ax_hal::irq::resolve_irq_source(...)` / platform resolver and register the returned `IrqId`. When ACPI supplies trigger/polarity/controller metadata, carry it as `IrqSource::AcpiGsiRoute` instead of flattening it to a bare `AcpiGsi`, because PCI INTx routes may use a low GSI with non-ISA level/low semantics. Likewise, FDT device bindings should carry the raw interrupt specifier plus its controller owner in `BindingIrqSource::FdtInterrupt` until the OS/platform layer can resolve that owner to a controller domain and configure it; do not expose parentless FDT cells from `irq-framework` or configure a controller in generic driver probe merely to obtain a legacy number. `rdif_intc` controllers must expose fallible `translate_fdt` / `translate_acpi` methods that return controller-local hardware line and trigger metadata; the registering platform allocates or looks up a domain owner entry for the concrete `rdrive::DeviceId`, passes that domain to `rdif_intc::Intc::new(domain, driver)`, and the wrapper combines that domain with the local `HwIrq` before `configure` / `configure_acpi` programs trigger, polarity, vector, or mask state. Platform `irq_set_enable` and `irq_set_affinity` paths must route by the incoming domain's registered owner/kind and return an error on missing controllers, lock failures, unsupported affinity, or backend/type mismatches instead of silently no-oping. Empty, malformed, out-of-range, or unsupported firmware specifiers must return `IrqError` instead of IRQ 0, a base vector, or a guessed legacy number. If an FDT PCI host bridge preconfigures a controller-level legacy INTx route, store that route as a native `BindingIrq` source (plus any temporary raw compatibility value) and let child endpoints reuse it before falling back to PCI `interrupt-map` parsing.
- **Domain expectations**: x86 LAPIC timer and IOAPIC are distinct domains, so trap vector `0x20` is not `AcpiGsi(0)`. On aarch64, GIC INTID is the `HwIrq` within the GIC domain. On riscv64, PLIC source is the `HwIrq` within the PLIC domain. On loongarch64, EIOINTC and PCH-PIC must remain separate domains. A platform that cannot resolve an `IrqSource` must return `IrqError::Unsupported` instead of guessing a numeric IRQ.
- **x86 QEMU IRQ contract**: the dynamic x86 path targets modern QEMU `q35` with ACPI/MADT, Local APIC or x2APIC, IOAPIC, and PCI INTx routing. Do not add 8259/PIC fallback, i440fx-specific IRQ assumptions, non-ACPI IRQ probing, raw GSI enable bypasses, or vector arithmetic outside the IOAPIC controller. LAPIC/x2APIC owns timer, IPI, EOI, and spurious handling; `X86IoApicIntc` owns external GSI route state, vector conflict checks, trigger/polarity, mask, and affinity updates through `rdif_intc::Intc`. x2APIC paths must preserve full `u32` APIC IDs for CPU-local operations, while xAPIC and IOAPIC physical destinations must reject APIC IDs that cannot be encoded without truncation.
- **Runtime console selection**: Dynamic platforms expose the firmware-selected hardware console through `somehal::console_device_id()` and `ax_hal::console::device_id()`. The value is `Result<rdrive::DeviceId, ConsoleDeviceIdError>` derived from bootargs `console=`, ACPI SPCR, or FDT `stdout-path`; static platforms return `Err(NotSpecified)`. OS code such as Starry should match `Ok(id)` against probed serial devices, use `ttyS0` as the Linux-style hardware-console fallback only for `Err(NotSpecified)`, and leave `/dev/console` unbound (`ENODEV`) for non-hardware console selections, unmatched selected hardware devices, or when no serial console TTY exists. Do not reparse FDT or bootargs in the tty layer.
- **Runtime console ownership**: once Starry or another OS runtime binds the firmware-selected UART to an interrupt-driven tty/serial driver, call `ax_hal::console::claim_runtime_output()` and stop the low-level boot/platform console path from writing the same UART registers directly. The hardware console must have one runtime register owner; otherwise kernel log output and tty output can interleave at the UART register level and corrupt test markers or user input/output.
- **Dynamic firmware devices**: for `rdrive` ACPI probes, real non-empty ACPI ID lists enumerate namespace `Device` nodes and expose `_CRS` memory, I/O port, and IRQ resources through `AcpiInfo`; empty ID lists or synthetic root IDs are reserved for root-table style callbacks.
Expand All @@ -52,6 +53,7 @@ Current Axvisor LoongArch QEMU bring-up uses the dynamic UEFI platform path. The
- Allocate and align boot stack, per-CPU areas, secondary stacks, boot arguments, and page tables before enabling SMP.
- Install trap vectors before enabling interrupts, timer interrupts, MMU faults, or secondary CPU execution.
- On x86 QEMU, do not trust CPUID timing leaves unless the reported TSC frequency is plausible; some virtual CPU combinations expose invalid zero or tiny values. Prefer a trusted hypervisor timing leaf, then CPUID timing data, then PIT-based TSC calibration before falling back to processor base frequency.
- On x86 QEMU, initialize LAPIC/x2APIC once and keep APIC IDs as firmware IDs, not logical CPU indices. Use x2APIC MSRs when x2APIC is enabled, bound IPI delivery waits, reject xAPIC AP startup/IPI destinations above 255, and keep external IOAPIC INTx programming in the runtime `X86IoApicIntc` path instead of someboot or HAL bypass helpers.
- On AArch64, keep the someboot `hv` feature scoped to the EL2 kernel path. For non-`hv` EL1 boot, choose the EL1 arch timer at runtime from the boot EL: use CNTP when EL2 is available and CNTV when EL2 is unavailable, and keep the FDT timer interrupt index consistent with the selected mode.
- Build page tables for identity/firmware access, direct map, kernel high map, MMIO, and per-CPU data as the arch requires.
- Flush TLB/cache and use architecture barriers around page table writes, boot argument writes, and secondary CPU release.
Expand Down
3 changes: 3 additions & 0 deletions components/irq-framework/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,9 @@ impl<O: IrqOps> Registry<O> {
self.ops.set_enabled(irq, Some(cpu), enabled)
}
Some(cpu) => {
if self.ops.in_irq_context() {
return Err(IrqError::InIrqContext);
}
let mut request = RemoteEnable {
registry: self as *const Self as *mut (),
irq,
Expand Down
2 changes: 2 additions & 0 deletions components/irq-framework/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ pub enum IrqError {
InvalidCpu,
/// The target CPU is offline.
CpuOffline,
/// A synchronous IRQ operation timed out.
Timeout,
/// IRQ line/action sharing rules reject the operation.
Busy,
/// Allocation failed.
Expand Down
34 changes: 34 additions & 0 deletions components/irq-framework/tests/std_sim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,40 @@ fn remote_per_cpu_enable_uses_run_on_cpu_sync() {
}));
}

#[test]
fn remote_per_cpu_enable_from_irq_context_is_rejected_without_ipi() {
let ops = MockOps::with_cpus(4);
ops.set_current_cpu(0);
ops.set_line_enabled(13, Some(2), false);
let registry = Registry::new(ops.clone());
let counter = AtomicUsize::new(0);
let data = NonNull::from(&counter).cast();

let handle = registry
.request(
irq(13),
IrqRequest::new(count_handler, data)
.scope(IrqScope::PerCpu {
cpus: CpuMask::from_cpu(CpuId(2)),
})
.auto_enable(AutoEnable::No),
)
.unwrap();
ops.inner.remote_calls.store(0, Ordering::SeqCst);
ops.clear_calls();

ops.set_in_irq(true);
assert_eq!(registry.enable(handle), Err(IrqError::InIrqContext));
ops.set_in_irq(false);

assert_eq!(ops.inner.remote_calls.load(Ordering::SeqCst), 0);
assert!(!ops.calls().contains(&OpCall::SetEnabled {
irq: 13,
cpu: Some(2),
enabled: true,
}));
}

#[test]
fn failed_per_cpu_enable_rolls_back_action_state() {
let ops = MockOps::with_cpus(4);
Expand Down
128 changes: 106 additions & 22 deletions components/someboot/src/arch/x86_64/power.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use core::{
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
};

use x86::msr::{IA32_APIC_BASE, rdmsr};
use x86::msr::{
IA32_APIC_BASE, IA32_X2APIC_APICID, IA32_X2APIC_ESR, IA32_X2APIC_ICR, rdmsr, wrmsr,
};

use crate::{mem::phys_to_virt, power::CpuOnError, smp::PerCpuMeta};

Expand All @@ -16,6 +18,9 @@ const AP_START_TIMEOUT_US: u64 = 500_000;
const LAPIC_REG_ESR: u32 = 0x280;
const LAPIC_REG_ICR_LOW: u32 = 0x300;
const LAPIC_REG_ICR_HIGH: u32 = 0x310;
const ICR_DELIVERY_PENDING: u32 = 1 << 12;
const IPI_DELIVERY_WAIT_SPINS: usize = 1_000_000;
const IA32_APIC_BASE_X2APIC_ENABLE: u64 = 1 << 10;

// INIT IPI (level-triggered): assert then deassert.
const ICR_INIT_ASSERT: u32 = 0x0000_c500;
Expand All @@ -26,6 +31,12 @@ const ICR_STARTUP_BASE: u32 = 0x0000_4600;
static START_LOCK: AtomicBool = AtomicBool::new(false);
static AP_BOOTED_ID: AtomicUsize = AtomicUsize::new(usize::MAX);

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ApicMode {
XApic,
X2Apic,
}

global_asm!(
r#"
.section .text.ap_trampoline, "ax"
Expand Down Expand Up @@ -136,17 +147,20 @@ pub(crate) fn notify_ap_started(apic_id: usize) {
}

fn current_apic_id() -> usize {
x86::cpuid::CpuId::new()
.get_feature_info()
.map(|info| info.initial_local_apic_id() as usize)
.unwrap_or(0)
match current_apic_mode() {
ApicMode::X2Apic => unsafe { rdmsr(IA32_X2APIC_APICID) as usize },
ApicMode::XApic => x86::cpuid::CpuId::new()
.get_feature_info()
.map(|info| info.initial_local_apic_id() as usize)
.unwrap_or(0),
}
}

pub(crate) fn cpu_on(apic_id: usize, entry: usize, arg: usize) -> Result<(), CpuOnError> {
if apic_id == current_apic_id() {
return Err(CpuOnError::AlreadyOn);
}
let apic_id = u8::try_from(apic_id).map_err(|_| CpuOnError::InvalidParameters)?;
let apic_id = u32::try_from(apic_id).map_err(|_| CpuOnError::InvalidParameters)?;
let meta = unsafe { &*(phys_to_virt(arg) as *const PerCpuMeta) };
if meta.boot_table_paddr > u32::MAX as usize {
return Err(CpuOnError::Other(anyhow::anyhow!(
Expand All @@ -165,21 +179,13 @@ pub(crate) fn cpu_on(apic_id: usize, entry: usize, arg: usize) -> Result<(), Cpu
entry_virt as u64,
);

unsafe {
send_ipi(apic_id, ICR_INIT_ASSERT);
}
send_ipi(apic_id, ICR_INIT_ASSERT)?;
delay_us(10_000);
unsafe {
send_ipi(apic_id, ICR_INIT_DEASSERT);
}
send_ipi(apic_id, ICR_INIT_DEASSERT)?;
delay_us(200);
unsafe {
send_ipi(apic_id, ICR_STARTUP_BASE | AP_TRAMPOLINE_VECTOR as u32);
}
send_ipi(apic_id, ICR_STARTUP_BASE | AP_TRAMPOLINE_VECTOR as u32)?;
delay_us(200);
unsafe {
send_ipi(apic_id, ICR_STARTUP_BASE | AP_TRAMPOLINE_VECTOR as u32);
}
send_ipi(apic_id, ICR_STARTUP_BASE | AP_TRAMPOLINE_VECTOR as u32)?;

let start = super::trap::ticks_now();
let timeout_ticks = us_to_tsc_ticks(AP_START_TIMEOUT_US);
Expand Down Expand Up @@ -294,16 +300,72 @@ unsafe fn lapic_read(offset: u32) -> u32 {
unsafe { ptr.read_volatile() }
}

unsafe fn send_ipi(apic_id: u8, icr_low: u32) {
fn current_apic_mode() -> ApicMode {
let base = unsafe { rdmsr(IA32_APIC_BASE) };
if base & IA32_APIC_BASE_X2APIC_ENABLE != 0 {
ApicMode::X2Apic
} else {
ApicMode::XApic
}
}

fn xapic_destination(apic_id: u32) -> Result<u32, CpuOnError> {
let dest = u8::try_from(apic_id).map_err(|_| CpuOnError::InvalidParameters)?;
Ok(u32::from(dest) << 24)
}

fn x2apic_icr(apic_id: u32, icr_low: u32) -> u64 {
(u64::from(apic_id) << 32) | u64::from(icr_low)
}

fn send_ipi(apic_id: u32, icr_low: u32) -> Result<(), CpuOnError> {
match current_apic_mode() {
ApicMode::X2Apic => send_x2apic_ipi(x2apic_icr(apic_id, icr_low)),
ApicMode::XApic => send_xapic_ipi(xapic_destination(apic_id)?, icr_low),
}
}

fn send_xapic_ipi(destination: u32, icr_low: u32) -> Result<(), CpuOnError> {
unsafe {
lapic_write(LAPIC_REG_ESR, 0);
lapic_write(LAPIC_REG_ESR, 0);
lapic_write(LAPIC_REG_ICR_HIGH, (apic_id as u32) << 24);
lapic_write(LAPIC_REG_ICR_HIGH, destination);
lapic_write(LAPIC_REG_ICR_LOW, icr_low);
while lapic_read(LAPIC_REG_ICR_LOW) & (1 << 12) != 0 {
spin_loop();
}
wait_xapic_delivery()
}

fn send_x2apic_ipi(icr: u64) -> Result<(), CpuOnError> {
unsafe {
wrmsr(IA32_X2APIC_ESR, 0);
wrmsr(IA32_X2APIC_ESR, 0);
wrmsr(IA32_X2APIC_ICR, icr);
}
wait_x2apic_delivery()
}

fn wait_xapic_delivery() -> Result<(), CpuOnError> {
for _ in 0..IPI_DELIVERY_WAIT_SPINS {
if unsafe { lapic_read(LAPIC_REG_ICR_LOW) } & ICR_DELIVERY_PENDING == 0 {
return Ok(());
}
spin_loop();
}
Err(CpuOnError::Other(anyhow::anyhow!(
"timeout waiting xAPIC IPI delivery"
)))
}

fn wait_x2apic_delivery() -> Result<(), CpuOnError> {
for _ in 0..IPI_DELIVERY_WAIT_SPINS {
if unsafe { rdmsr(IA32_X2APIC_ICR) } & u64::from(ICR_DELIVERY_PENDING) == 0 {
return Ok(());
}
spin_loop();
}
Err(CpuOnError::Other(anyhow::anyhow!(
"timeout waiting x2APIC IPI delivery"
)))
}

struct StartupGuard;
Expand All @@ -325,3 +387,25 @@ impl Drop for StartupGuard {
START_LOCK.store(false, Ordering::Release);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn xapic_destination_rejects_high_apic_ids_without_truncation() {
assert!(matches!(xapic_destination(0xff), Ok(0xff00_0000)));
assert!(matches!(
xapic_destination(0x100),
Err(CpuOnError::InvalidParameters)
));
}

#[test]
fn x2apic_icr_encodes_full_destination_id() {
let icr = x2apic_icr(0x1234_5678, ICR_STARTUP_BASE | AP_TRAMPOLINE_VECTOR as u32);

assert_eq!(icr >> 32, 0x1234_5678);
assert_eq!(icr as u32, ICR_STARTUP_BASE | AP_TRAMPOLINE_VECTOR as u32);
}
}
65 changes: 60 additions & 5 deletions components/someboot/src/arch/x86_64/trap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ const LAPIC_LVT_TIMER_TSC_DEADLINE: u32 = 1 << 18;
const LAPIC_SVR_ENABLE: u32 = 1 << 8;
const LAPIC_BASE_MASK: u64 = 0xffff_f000;
const IA32_APIC_BASE_ENABLE: u64 = 1 << 11;
const IA32_APIC_BASE_X2APIC_ENABLE: u64 = 1 << 10;
const LAPIC_TIMER_DIVIDE_BY_16: u32 = 0b0011;
const IA32_X2APIC_EOI: u32 = 0x80b;
const IA32_X2APIC_SIVR: u32 = 0x80f;
const IA32_X2APIC_LVT_TIMER: u32 = 0x832;
const IA32_X2APIC_INIT_COUNT: u32 = 0x838;
const IA32_X2APIC_CUR_COUNT: u32 = 0x839;
const IA32_X2APIC_DIV_CONF: u32 = 0x83e;
const PIT_CHANNEL2_PORT: u16 = 0x42;
const PIT_COMMAND_PORT: u16 = 0x43;
const PIT_CONTROL_PORT: u16 = 0x61;
Expand All @@ -52,6 +59,12 @@ static LAPIC_READY: AtomicBool = AtomicBool::new(false);
static TSC_INFO_STATE: AtomicU8 = AtomicU8::new(0);
static IDT_STATE: AtomicU8 = AtomicU8::new(0);

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ApicMode {
XApic,
X2Apic,
}

#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct InterruptStackFrame {
Expand Down Expand Up @@ -338,6 +351,9 @@ fn load_idt() {
fn init_lapic() {
let mut base = unsafe { rdmsr(msr::IA32_APIC_BASE) };
base |= IA32_APIC_BASE_ENABLE;
if cpu_has_x2apic() {
base |= IA32_APIC_BASE_X2APIC_ENABLE;
}
unsafe {
wrmsr(msr::IA32_APIC_BASE, base);
}
Expand Down Expand Up @@ -423,14 +439,53 @@ fn ticks_to_apic_counts(ticks: u64) -> u32 {
}

fn read_lapic_reg(offset: u32) -> u32 {
let ptr = lapic_ptr(offset);
unsafe { ptr.read_volatile() }
match current_apic_mode() {
ApicMode::X2Apic => unsafe { rdmsr(x2apic_msr(offset)) as u32 },
ApicMode::XApic => {
let ptr = lapic_ptr(offset);
unsafe { ptr.read_volatile() }
}
}
}

fn write_lapic_reg(offset: u32, value: u32) {
let ptr = lapic_ptr(offset);
unsafe {
ptr.write_volatile(value);
match current_apic_mode() {
ApicMode::X2Apic => unsafe {
wrmsr(x2apic_msr(offset), u64::from(value));
},
ApicMode::XApic => {
let ptr = lapic_ptr(offset);
unsafe {
ptr.write_volatile(value);
}
}
}
}

fn cpu_has_x2apic() -> bool {
CpuId::new()
.get_feature_info()
.is_some_and(|info| info.has_x2apic())
}

fn current_apic_mode() -> ApicMode {
let base = unsafe { rdmsr(msr::IA32_APIC_BASE) };
if base & IA32_APIC_BASE_X2APIC_ENABLE != 0 {
ApicMode::X2Apic
} else {
ApicMode::XApic
}
}

fn x2apic_msr(offset: u32) -> u32 {
match offset {
LAPIC_REG_EOI => IA32_X2APIC_EOI,
LAPIC_REG_SVR => IA32_X2APIC_SIVR,
LAPIC_REG_LVT_TIMER => IA32_X2APIC_LVT_TIMER,
LAPIC_REG_TIMER_INIT_COUNT => IA32_X2APIC_INIT_COUNT,
LAPIC_REG_TIMER_CUR_COUNT => IA32_X2APIC_CUR_COUNT,
LAPIC_REG_TIMER_DIV => IA32_X2APIC_DIV_CONF,
_ => panic!("unsupported x2APIC register offset {offset:#x}"),
}
}

Expand Down
5 changes: 0 additions & 5 deletions os/arceos/modules/axhal/src/irq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ pub fn handle_irq(vector: usize) -> bool {
handled
}

#[cfg(all(plat_dyn, target_arch = "x86_64"))]
pub fn set_ioapic_gsi_enabled_from_irq(gsi: u32, enabled: bool) -> Result<(), IrqError> {
axplat_dyn::set_ioapic_gsi_enabled_from_irq(gsi, enabled)
}

/// Installs the default ArceOS IRQ dispatcher into `ax-cpu`'s runtime hook.
///
/// This is intended for runtimes that dispatch traps through
Expand Down
Loading
Loading