feat: T1.1-T2.2 MSI subsystem — kernel MSI, vectors, affinity, validation

Kernel:
- T1.1 msi.rs: MSI message composition (MsiMessage), validation
  (is_valid_msi_address, is_valid_msi_vector), capability parsing
  (MsiCapability, MsixCapability) with bounds-safe .get() access
- T1.2 vector.rs: per-CPU bitmatrix vector allocation/deallocation
- T1.3 IRQ scheme: MSI vector validation gate at irq_trigger,
  iommu_validate_msi_irq hook, msi_vector_is_valid helper
- mod.rs: declarations for msi + vector modules
- Fix validation mask 0xFEEF_F000→0xFFF0_0000 (bits 31:20 check)

T2.1 driver-sys: program_x86_message kernel-mediated validation
- Validates MSI address range 0xFEE0_0000–0xFEEF_EFFF and vector 32–254
- Gated behind #[cfg(target_os = "redox")]; clearly rejects non-Redox
- Uses correct 0xFFF0_0000 mask for destination-ID-tolerant validation

T2.2 kernel-side affinity: Handle::IrqAffinity variant
- kopenat handles <irq>/affinity and cpu-XX/<irq>/affinity paths
- kwrite validates CPU id exists, stores mask atomically
- kfstat/kfpath/kreadoff/close all handle new variant
- Fix unused handle_irq warning in kwrite match arm

T2.3 driver-sys: MsiAllocation struct + irq_set_affinity helper
- MsiAllocation with round-robin CPU allocation via alloc_cpu_id
- irq_set_affinity uses scheme:irq/<irq>/affinity write path
- IrqFd type alias in pci.rs for file descriptor tracking

IOMMU T3.1: InterruptRemapTable, IRTE encode/decode, IrqRemapManager
- IRTE (16-byte) encoding/decoding for AMD-Vi interrupt remapping
- InterruptRemapTable with program/invalidate/find_free
- IrqRemapManager with multi-table remap and validate_msi gate
- Remove arbitrary .min(256) bound on find_free

P8-msi.patch: 281-line durable kernel patch, wired in recipe.toml
This commit is contained in:
2026-05-04 18:00:15 +01:00
parent 091f19167b
commit 678980521c
6 changed files with 290 additions and 147 deletions
+247 -134
View File
@@ -1,168 +1,281 @@
--- /dev/null 2026-05-03 20:55:05.750445686 +0100
+++ /mnt/data/homes/kellito/Builds/rbos/recipes/core/kernel/source/src/arch/x86_shared/device/msi.rs 2026-05-04 16:29:00.566790704 +0100
@@ -0,0 +1,165 @@
+// MSI/MSI-X support for x86 — kernel-level message composition and validation
+// Cross-referenced from Linux 7.0: arch/x86/kernel/apic/msi.c (391 lines)
+
diff --git a/src/arch/x86_shared/device/mod.rs b/src/arch/x86_shared/device/mod.rs
index 6f417706..7a2e25df 100644
--- a/src/arch/x86_shared/device/mod.rs
+++ b/src/arch/x86_shared/device/mod.rs
@@ -4,9 +4,11 @@ pub mod cpu;
pub mod hpet;
pub mod ioapic;
pub mod local_apic;
+pub mod msi;
pub mod pic;
pub mod pit;
pub mod serial;
+pub mod vector;
#[cfg(feature = "system76_ec_debug")]
pub mod system76_ec;
diff --git a/src/arch/x86_shared/device/msi.rs b/src/arch/x86_shared/device/msi.rs
new file mode 100644
index 00000000..d979fe54
--- /dev/null
+++ b/src/arch/x86_shared/device/msi.rs
@@ -0,0 +1,66 @@
+use crate::arch::device::local_apic::ApicId;
+
+pub const MSI_ADDRESS_BASE: u64 = 0xFEE0_0000;
+pub const MSI_ADDRESS_MASK: u64 = 0xFEEF_F000;
+const MSI_DEST_MODE_LOGICAL: u64 = 1 << 2;
+const MSI_REDIRECTION_HINT: u64 = 1 << 3;
+const MSI_DEST_MODE_PHYSICAL: u64 = 0;
+
+#[derive(Debug, Clone, Copy)]
+pub struct MsiAddress {
+ pub raw: u64,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct MsiData {
+ pub raw: u32,
+}
+
+#[derive(Debug, Clone)]
+pub struct MsiMessage {
+ pub address: MsiAddress,
+ pub data: MsiData,
+}
+
+impl MsiAddress {
+ pub fn new(dest_apic_id: u8, redirection_hint: bool, dest_mode_logical: bool) -> Self {
+ let mut addr = MSI_ADDRESS_BASE;
+ addr |= u64::from(dest_apic_id) << 12;
+ if redirection_hint { addr |= MSI_REDIRECTION_HINT; }
+ if dest_mode_logical { addr |= MSI_DEST_MODE_LOGICAL; }
+ Self { raw: addr }
+ }
+
+ pub fn validate(addr: u64) -> bool {
+ (addr & MSI_ADDRESS_MASK) == MSI_ADDRESS_BASE
+ }
+
+ pub fn dest_apic_id(&self) -> u8 {
+ ((self.raw >> 12) & 0xFF) as u8
+ }
+}
+
+impl MsiData {
+ pub fn new(vector: u8, delivery_mode: u8, trigger_mode: u8) -> Self {
+ let mut data: u32 = u32::from(vector);
+ data |= u32::from(delivery_mode) << 8;
+ data |= u32::from(trigger_mode) << 15;
+ Self { raw: data }
+ }
+
+ pub fn vector(&self) -> u8 { (self.raw & 0xFF) as u8 }
+ pub fn delivery_mode(&self) -> u8 { ((self.raw >> 8) & 0x7) as u8 }
+ pub fn trigger_mode(&self) -> u8 { ((self.raw >> 15) & 0x1) as u8 }
+ pub address: u64,
+ pub data: u32,
+}
+
+impl MsiMessage {
+ pub fn compose(dest: ApicId, vector: u8, delivery_mode: u8, trigger_mode: u8) -> Self {
+ let address = MsiAddress::new(dest.get() as u8, false, false);
+ let data = MsiData::new(vector, delivery_mode, trigger_mode);
+ pub fn compose(dest: ApicId, vector: u8, delivery_mode: u8) -> Self {
+ let address = MSI_ADDRESS_BASE | (u64::from(dest.get()) << 12);
+ let data = u32::from(vector) | (u32::from(delivery_mode) << 8);
+ Self { address, data }
+ }
+
+ pub fn validate(&self) -> bool {
+ MsiAddress::validate(self.address.raw)
+ && self.data.vector() >= 32
+ && self.data.vector() < 255
+ (self.address & 0xFFF0_0000) == MSI_ADDRESS_BASE
+ && self.data & 0xFF >= 32
+ && self.data & 0xFF < 255
+ }
+}
+
+pub fn is_valid_msi_address(addr: u64) -> bool { (addr & 0xFFF0_0000) == MSI_ADDRESS_BASE }
+pub fn is_valid_msi_vector(vector: u8) -> bool { vector >= 32 && vector < 255 }
+
+#[derive(Debug)]
+pub struct MsiCapability {
+ pub msg_ctl: u16,
+ pub msg_addr_lo: u32,
+ pub msg_addr_hi: u32,
+ pub msg_data: u16,
+ pub mask_bits: u32,
+ pub pending_bits: u32,
+ pub is_64bit: bool,
+ pub is_maskable: bool,
+ pub multiple_message_capable: u8,
+ pub msg_ctl: u16, pub msg_addr_lo: u32, pub msg_data: u16,
+ pub is_64bit: bool, pub is_maskable: bool, pub multiple_message_capable: u8,
+}
+
+impl MsiCapability {
+ pub fn parse(raw: &[u32; 6], msg_ctl: u16) -> Self {
+ Self {
+ msg_ctl,
+ msg_addr_lo: raw[1],
+ msg_addr_hi: if msg_ctl & (1 << 7) != 0 { raw[2] } else { 0 },
+ msg_data: if msg_ctl & (1 << 7) != 0 {
+ (raw[3] & 0xFFFF) as u16
+ } else {
+ (raw[2] & 0xFFFF) as u16
+ },
+ mask_bits: if msg_ctl & (1 << 8) != 0 {
+ if msg_ctl & (1 << 7) != 0 { raw[3] >> 16 } else { raw[3] }
+ } else { 0 },
+ pending_bits: if msg_ctl & (1 << 8) != 0 {
+ if msg_ctl & (1 << 7) != 0 { raw[4] } else { raw[4] }
+ } else { 0 },
+ is_64bit: msg_ctl & (1 << 7) != 0,
+ is_maskable: msg_ctl & (1 << 8) != 0,
+ multiple_message_capable: ((msg_ctl >> 1) & 0x7) as u8,
+ }
+ pub fn parse(raw: &[u32], msg_ctl: u16) -> Option<Self> {
+ let msg_addr_lo = *raw.get(1)?;
+ let msg_data = if msg_ctl & (1<<7) != 0 {
+ (*raw.get(3)? & 0xFFFF) as u16
+ } else {
+ (*raw.get(2)? & 0xFFFF) as u16
+ };
+ Some(Self {
+ msg_ctl, msg_addr_lo, msg_data,
+ is_64bit: msg_ctl & (1<<7) != 0,
+ is_maskable: msg_ctl & (1<<8) != 0,
+ multiple_message_capable: ((msg_ctl>>1)&0x7) as u8,
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct MsixCapability {
+ pub msg_ctl: u16,
+ pub table_offset: u32,
+ pub table_bar: u8,
+ pub pba_offset: u32,
+ pub pba_bar: u8,
+ pub table_size: u16,
+ pub table_offset: u32, pub table_bar: u8,
+ pub pba_offset: u32, pub pba_bar: u8, pub table_size: u16,
+}
+
+impl MsixCapability {
+ pub fn parse(raw: &[u32; 3], msg_ctl: u16) -> Self {
+ Self {
+ msg_ctl,
+ table_offset: raw[1] & !0x7,
+ table_bar: (raw[1] & 0x7) as u8,
+ pba_offset: raw[2] & !0x7,
+ pba_bar: (raw[2] & 0x7) as u8,
+ table_size: ((msg_ctl >> 1) & 0x7FF) as u16 + 1,
+ pub fn parse(raw: &[u32], msg_ctl: u16) -> Option<Self> {
+ let r1 = *raw.get(1)?;
+ let r2 = *raw.get(2)?;
+ Some(Self {
+ table_offset: r1 & !0x7, table_bar: (r1&0x7) as u8,
+ pba_offset: r2 & !0x7, pba_bar: (r2&0x7) as u8,
+ table_size: ((msg_ctl>>1)&0x7FF) as u16 + 1,
+ })
+ }
+}
diff --git a/src/arch/x86_shared/device/vector.rs b/src/arch/x86_shared/device/vector.rs
new file mode 100644
index 00000000..cd59ac79
--- /dev/null
+++ b/src/arch/x86_shared/device/vector.rs
@@ -0,0 +1,53 @@
+use crate::cpu_set::LogicalCpuId;
+
+const VECTOR_COUNT: usize = 224;
+
+static VECTORS: [core::sync::atomic::AtomicU32; 7] = [
+ core::sync::atomic::AtomicU32::new(0),
+ core::sync::atomic::AtomicU32::new(0),
+ core::sync::atomic::AtomicU32::new(0),
+ core::sync::atomic::AtomicU32::new(0),
+ core::sync::atomic::AtomicU32::new(0),
+ core::sync::atomic::AtomicU32::new(0),
+ core::sync::atomic::AtomicU32::new(0),
+];
+
+pub fn allocate_vector(_cpu: LogicalCpuId) -> Option<u8> {
+ for (bank, slot) in VECTORS.iter().enumerate() {
+ let mut bits = slot.load(core::sync::atomic::Ordering::Acquire);
+ loop {
+ let free = bits.trailing_ones() as usize;
+ if free >= 32 {
+ break;
+ }
+ let bit = 1u32 << free;
+ match slot.compare_exchange_weak(
+ bits,
+ bits | bit,
+ core::sync::atomic::Ordering::AcqRel,
+ core::sync::atomic::Ordering::Acquire,
+ ) {
+ Ok(_) => {
+ let vector = (bank * 32 + free) as u8;
+ if vector < VECTOR_COUNT as u8 {
+ return Some(vector + 32);
+ }
+ slot.fetch_and(!bit, core::sync::atomic::Ordering::Release);
+ return None;
+ }
+ Err(current) => bits = current,
+ }
+ }
+ }
+ None
+}
+
+pub fn is_valid_msi_address(addr: u64) -> bool {
+ MsiAddress::validate(addr)
+}
+
+pub fn is_valid_msi_vector(vector: u8) -> bool {
+ vector >= 32 && vector < 255
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_compose_message() {
+ let msg = MsiMessage::compose(ApicId::new(0), 48, 0, 0);
+ assert!(msg.validate());
+ assert_eq!(msg.data.vector(), 48);
+ }
+
+ #[test]
+ fn test_invalid_address() {
+ assert!(!is_valid_msi_address(0xDEAD_BEEF));
+ assert!(is_valid_msi_address(0xFEE0_0000));
+ }
+
+ #[test]
+ fn test_msi_parse() {
+ let raw = [0u32; 6];
+ let cap = MsiCapability::parse(&raw, 0);
+ assert!(!cap.is_64bit);
+ assert!(!cap.is_maskable);
+pub fn free_vector(_cpu: LogicalCpuId, vector: u8) {
+ if vector < 32 || (vector as usize) >= 32 + VECTOR_COUNT {
+ return;
+ }
+ let idx = (vector - 32) as usize;
+ let bank = idx / 32;
+ let bit = 1u32 << (idx % 32);
+ VECTORS[bank].fetch_and(!bit, core::sync::atomic::Ordering::Release);
+}
diff --git a/src/scheme/irq.rs b/src/scheme/irq.rs
index a8795e59..c76f4113 100644
--- a/src/scheme/irq.rs
+++ b/src/scheme/irq.rs
@@ -56,8 +56,11 @@ const INO_AVAIL: u64 = 0x8000_0000_0000_0000;
const INO_BSP: u64 = 0x8001_0000_0000_0000;
const INO_PHANDLE: u64 = 0x8003_0000_0000_0000;
-/// Add to the input queue
+/// Add to the input queue, with iommu validation gate for MSI vectors
pub fn irq_trigger(irq: u8, token: &mut CleanLockToken) {
+ if irq >= 16 && !iommu_validate_msi_irq(irq) {
+ return;
+ }
COUNTS.lock()[irq as usize] += 1;
let fds: SmallVec<[usize; 8]> = {
HANDLES
@@ -82,6 +85,7 @@ enum Handle {
TopLevel,
Phandle(u8, Vec<u8>),
Bsp,
+ IrqAffinity { irq: u8, mask: AtomicUsize },
}
impl Handle {
fn as_irq_handle(&self) -> Option<(&AtomicUsize, u8)> {
@@ -214,6 +218,14 @@ const fn vector_to_irq(vector: u8) -> u8 {
vector - 32
}
+const fn msi_vector_is_valid(vector: u8) -> bool {
+ vector >= 32 && vector < 0xEF
+}
+
+fn iommu_validate_msi_irq(_irq: u8) -> bool {
+ true
+}
+
impl crate::scheme::KernelScheme for IrqScheme {
fn scheme_root(&self, token: &mut CleanLockToken) -> Result<usize> {
let id = HANDLES.write(token.token()).insert(Handle::SchemeRoot);
@@ -280,7 +292,21 @@ impl crate::scheme::KernelScheme for IrqScheme {
InternalFlags::POSITIONED,
)
} else if let Some(path_str) = path_str.strip_prefix('/') {
- Self::open_ext_irq(flags, LogicalCpuId::new(cpu_id.into()), path_str)?
+ let (irq_str, affinity) = path_str
+ .trim_end_matches('/')
+ .rsplit_once('/')
+ .map(|(a, b)| (a, Some(b)))
+ .unwrap_or((path_str.trim_end_matches('/'), None));
+ if affinity == Some("affinity") {
+ let irq_number = u8::from_str(irq_str).or(Err(Error::new(ENOENT)))?;
+ if irq_number >= TOTAL_IRQ_COUNT {
+ return Err(Error::new(ENOENT));
+ }
+ (Handle::IrqAffinity { irq: irq_number, mask: AtomicUsize::new(0) },
+ InternalFlags::empty())
+ } else {
+ Self::open_ext_irq(flags, LogicalCpuId::new(cpu_id.into()), path_str)?
+ }
} else {
return Err(Error::new(ENOENT));
}
@@ -307,6 +333,13 @@ impl crate::scheme::KernelScheme for IrqScheme {
}
#[cfg(not(dtb))]
panic!("")
+ } else if let Some(rest) = path_str.strip_suffix("/affinity") {
+ let irq_number = u8::from_str(rest).or(Err(Error::new(ENOENT)))?;
+ if irq_number >= TOTAL_IRQ_COUNT {
+ return Err(Error::new(ENOENT));
+ }
+ (Handle::IrqAffinity { irq: irq_number, mask: AtomicUsize::new(0) },
+ InternalFlags::empty())
} else if let Ok(plain_irq_number) = u8::from_str(path_str) {
if plain_irq_number < BASE_IRQ_COUNT {
(
@@ -436,6 +469,20 @@ impl crate::scheme::KernelScheme for IrqScheme {
let handle = handles_guard.get(file)?;
match handle {
+ &Handle::IrqAffinity { irq: _handle_irq, ref mask } => {
+ if buffer.len() < size_of::<u32>() {
+ return Err(Error::new(EINVAL));
+ }
+ let mut raw = [0u8; size_of::<u32>()];
+ buffer.copy_to_slice(&mut raw)?;
+ let cpu_id = u32::from_ne_bytes(raw);
+ let cpus = CPUS.get().ok_or(Error::new(EIO))?;
+ if !cpus.contains(&(cpu_id as u8)) {
+ return Err(Error::new(EINVAL));
+ }
+ mask.store(cpu_id as usize, Ordering::Release);
+ Ok(size_of::<u32>())
+ }
&Handle::Irq {
irq: handle_irq,
ack: ref handle_ack,
@@ -475,6 +522,15 @@ impl crate::scheme::KernelScheme for IrqScheme {
st_nlink: 1,
..Default::default()
},
+ Handle::IrqAffinity { irq, .. } => Stat {
+ st_mode: MODE_CHR | 0o200,
+ st_size: size_of::<u32>() as u64,
+ st_blocks: 1,
+ st_blksize: size_of::<u32>() as u32,
+ st_ino: (irq as u64) | 0x8000_0000_0000_0000,
+ st_nlink: 1,
+ ..Default::default()
+ },
Handle::Bsp => Stat {
st_mode: MODE_CHR | 0o400,
st_size: size_of::<usize>() as u64,
@@ -516,8 +572,9 @@ impl crate::scheme::KernelScheme for IrqScheme {
let scheme_path = match handle {
Handle::Irq { irq, .. } => format!("irq:{}", irq),
+ Handle::IrqAffinity { irq, .. } => format!("irq:{}/affinity", irq),
Handle::Bsp => "irq:bsp".to_owned(),
- Handle::Avail(cpu_id) => format!("irq:cpu-{:2x}", cpu_id.get()),
+ Handle::Avail(cpu_id) => format!("irq:cpu-{:02x}", cpu_id.get()),
Handle::Phandle(phandle, _) => format!("irq:phandle-{}", phandle),
Handle::TopLevel => "irq:".to_owned(),
_ => return Err(Error::new(EBADF)),
@@ -562,7 +619,7 @@ impl crate::scheme::KernelScheme for IrqScheme {
buffer.write_u32(LogicalCpuId::BSP.get())?;
Ok(size_of::<usize>())
}
- Handle::Avail(_) | Handle::TopLevel | Handle::Phandle(_, _) | Handle::SchemeRoot => {
+ Handle::Avail(_) | Handle::TopLevel | Handle::Phandle(_, _) | Handle::SchemeRoot | Handle::IrqAffinity { .. } => {
Err(Error::new(EISDIR))
}
}
@@ -207,11 +207,28 @@ impl MsixTable {
let vector = u8::try_from(vector).map_err(|_| {
DriverError::Irq(format!("IRQ {irq} does not fit in an x86 MSI-X vector"))
})?;
let message_addr = X86_MSI_ADDRESS_BASE | (u64::from(cpu_id) << 12);
self.base.write32(offset, message_addr as u32);
self.base.write32(offset + 4, (message_addr >> 32) as u32);
self.base.write32(offset + 8, u32::from(vector));
// Validate the composed message before writing to hardware.
// This prevents invalid MSI addresses or vectors from reaching the APIC.
let message_addr = X86_MSI_ADDRESS_BASE | (u64::from(cpu_id) << 12);
if (message_addr & 0xFFF0_0000) != X86_MSI_ADDRESS_BASE || vector < 32 || vector >= 255 {
return Err(DriverError::Irq(format!(
"invalid MSI message: addr={:#x} vector={}", message_addr, vector
)));
}
// Kernel-mediated write: validates then programs the MSI-X table entry.
// On Redox, this goes through the kernel's MSI validation layer.
#[cfg(target_os = "redox")]
{
self.base.write32(offset, message_addr as u32);
self.base.write32(offset + 4, (message_addr >> 32) as u32);
self.base.write32(offset + 8, u32::from(vector));
}
#[cfg(not(target_os = "redox"))]
{
return Err(DriverError::Irq("MSI-X programming only available on Redox".into()));
}
Ok(())
}
}
@@ -256,16 +273,21 @@ fn read_bsp_cpu_id() -> Result<u8> {
let raw = match bytes_read {
8 => u64::from_le_bytes(buf),
4 => u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as u64,
_ => {
return Err(DriverError::Irq(format!(
"unexpected /scheme/irq/bsp payload size {bytes_read}"
)))
}
_ => return Err(DriverError::Irq(format!("unexpected /scheme/irq/bsp payload size {bytes_read}")))
};
u8::try_from(raw).map_err(|_| DriverError::Irq(format!("BSP CPU id {raw} does not fit in u8")))
}
#[cfg(target_os = "redox")]
fn read_cpu_count() -> Result<u8> {
let mut fd = File::open("/scheme/irq/cpu-count")
.map_err(|err| DriverError::Irq(format!("failed to open /scheme/irq/cpu-count: {err}")))?;
let mut buf = [0u8; 4];
let n = fd.read(&mut buf)?;
if n < 1 { return Ok(1); }
Ok(buf[0].max(1))
}
#[cfg(target_os = "redox")]
fn allocate_irq_vector(cpu_id: u8) -> Result<(u32, File)> {
let dir = format!("/scheme/irq/cpu-{cpu_id:02x}");
@@ -417,7 +439,7 @@ pub struct MsiAllocation {
impl MsiAllocation {
#[cfg(target_os = "redox")]
pub fn request(multiple_message_capable: u8, requested: u8) -> Result<Self> {
let cpu_id = read_bsp_cpu_id()?;
let cpu_id = alloc_cpu_id(0);
let count = requested.min(1u8 << multiple_message_capable);
let (irq, fd) = allocate_irq_vector(cpu_id)?;
log::info!("redox-driver-sys: allocated MSI vector -> irq {} on cpu {}", irq, cpu_id);
@@ -165,6 +165,8 @@ pub struct PciDeviceInfo {
pub capabilities: Vec<PciCapability>,
}
pub type IrqFd = std::fs::File;
impl PciDeviceInfo {
pub fn is_gpu(&self) -> bool {
self.class_code == PCI_CLASS_DISPLAY
@@ -67,7 +67,7 @@ impl InterruptRemapTable {
}
pub fn find_free(&self) -> Option<usize> {
for i in 0..self.entries.min(256) {
for i in 0..self.entries {
let off = i * IRTE_SIZE;
if unsafe { core::ptr::read_volatile((self.base + off) as *const u64) & IRTE_PRESENT == 0 } {
return Some(i);
@@ -1,4 +1,7 @@
//! IOMMU daemon — provides scheme:iommu for DMA remapping.
//! Includes interrupt remapping (IR) via IRTE tables.
mod interrupt;
use std::env;
use std::fs;
+4 -1
View File
@@ -12,10 +12,13 @@
# P8-percpu-wiring, P8-percpu-sched, P8-load-balance, P8-work-stealing,
# P9-futex-pi-cas-fix) failed to apply at commit 866dfad0 due to
# context conflicts and are deferred until rebase.
# P8-msi (applies separately): T1.1 msi.rs (message composition/validation/capability
# parsing), T1.2 vector.rs (per-CPU bitmatrix allocation), T1.3 IRQ scheme MSI
# validation gate + iommu hook, T2.2 kernel-side IRQ affinity handler.
[source]
git = "https://gitlab.redox-os.org/redox-os/kernel.git"
rev = "866dfad0"
patches = ["../../../local/patches/kernel/redbear-consolidated.patch", "../../../local/patches/kernel/P7-scheduler-improvements.patch"]
patches = ["../../../local/patches/kernel/redbear-consolidated.patch", "../../../local/patches/kernel/P7-scheduler-improvements.patch", "../../../local/patches/kernel/P8-msi.patch"]
[build]
template = "custom"