Skip to main content

strat9_bus_drivers/
scheme.rs

1//! Multi-driver VFS scheme served at `/bus/`.
2//!
3//! Each successfully-initialised bus driver appears as a sub-directory:
4//!
5//! ```text
6//! /bus/                     -> list of registered driver names + pci/
7//! /bus/pci/inventory        -> PCI device table
8//! /bus/pci/count            -> number of PCI devices
9//! /bus/pci/rescan           -> (write-only) refresh PCI cache
10//! /bus/pci/find/<vid>/<did> -> find devices by vendor/device
11//! /bus/pci/cfg/<b:d.f>/<off>/<w>  -> raw PCI config read
12//! /bus/<driver>/            -> driver info (compatible, errors, …)
13//! /bus/<driver>/status      -> driver status
14//! /bus/<driver>/error_count -> driver error count
15//! /bus/<driver>/reg/<hex>   -> read/write a driver register
16//!                              (NOTE: on software-defined buses without a
17//!                              register file, e.g. `moxtet`, `reg/<idx>` is
18//!                              a logical slot index, not an MMIO offset —
19//!                              see `moxtet::Moxtet` docs)
20//! /bus/<driver>/<child>     -> child-device info (if the driver reports any)
21//! ```
22
23use alloc::{boxed::Box, collections::BTreeMap, format, string::String, vec::Vec};
24use strat9_syscall::{
25    call,
26    data::{
27        DT_DIR, DT_REG, IPC_FILE_FLAG_DIRECTORY, IpcMessage, OPCODE_CLOSE, OPCODE_OPEN,
28        OPCODE_READ, OPCODE_READDIR, OPCODE_WRITE, OpenRequest, PCI_MATCH_DEVICE_ID,
29        PCI_MATCH_VENDOR_ID, PciAddress, PciDeviceInfo, PciProbeCriteria,
30    },
31    error::{EBADF, EINVAL, EIO, ENOENT, ENOMEM, ENOSYS, ENOTDIR},
32};
33
34use crate::BusDriver;
35
36/// Upper bound on simultaneously open handles.
37/// Prevents a single client from exhausting server memory by opening
38/// handles in a loop without closing them (DoS on the scheme server).
39const MAX_OPEN_HANDLES: usize = 256;
40
41/// Max simultaneously open handles **per sender**.
42/// Without it, one client could fill the global table and starve all
43/// others out of the scheme server.
44const MAX_HANDLES_PER_SENDER: usize = 64;
45
46/// Upper bound on cached `pci/find` results.
47///
48/// The `(vendor, device)` key space is 2^32: without a cap, a client
49/// reading many distinct find paths would grow the cache unboundedly
50/// (memory-exhaustion DoS on the scheme server).
51const MAX_FIND_CACHE_ENTRIES: usize = 64;
52
53// VFS scheme opcodes and typed request parsers: re-exported from strat9-abi
54// via strat9-syscall (single source of truth for the wire contract).
55const STATUS_OK: u32 = 0;
56
57/// Fixed reply prologue of a READ reply: `status` (4) + `count` (4).
58const READ_HEADER_SIZE: usize = 8;
59/// Fixed reply prologue of a READDIR reply: `status` (4) + `next_cursor`
60/// (2) + `count` (1) + `size` (1), written at payload offsets 0..8.
61const READDIR_HEADER_SIZE: usize = 8;
62/// Max inline data bytes carried by a READ reply:
63/// full payload minus the `status`/`count` prefix.
64const READ_DATA_CAPACITY: usize = IpcMessage::PAYLOAD_CAPACITY - READ_HEADER_SIZE;
65/// Max bytes usable for readdir entries: full payload minus the fixed
66/// reply prologue (`next_cursor` + `count` + `size`, written at 4..8).
67const READDIR_DATA_CAPACITY: usize = IpcMessage::PAYLOAD_CAPACITY - READDIR_HEADER_SIZE;
68
69// === Path constants ========================================================
70
71/// Driver-specific paths (relative to the driver prefix).
72const DRV_STATUS: &str = "status";
73const DRV_ERROR_COUNT: &str = "error_count";
74const DRV_SUSPEND: &str = "suspend";
75const DRV_RESUME: &str = "resume";
76const DRV_REG_PREFIX: &str = "reg/";
77const DRV_FIREWALL_DIR: &str = "firewall";
78const DRV_FIREWALL_INFO: &str = "firewall/info";
79const FIREWALL_GRANT_PREFIX: &str = "firewall/grant/";
80const FIREWALL_RELEASE_PREFIX: &str = "firewall/release/";
81
82/// Top-level paths.
83const PCI_PREFIX: &str = "pci";
84
85// === Handle ================================================================
86
87enum HandleKind {
88    /// Root : listing drivers + pci.
89    Root,
90    /// PCI sub-tree.
91    Pci(String),
92    /// A specific driver, with an optional sub-path.
93    Driver { driver_idx: usize, sub_path: String },
94}
95
96impl Clone for HandleKind {
97    fn clone(&self) -> Self {
98        match self {
99            HandleKind::Root => HandleKind::Root,
100            HandleKind::Pci(p) => HandleKind::Pci(p.clone()),
101            HandleKind::Driver {
102                driver_idx,
103                sub_path,
104            } => HandleKind::Driver {
105                driver_idx: *driver_idx,
106                sub_path: sub_path.clone(),
107            },
108        }
109    }
110}
111
112struct OpenHandle {
113    kind: HandleKind,
114    /// PID/TID of the process that opened this handle.
115    ///
116    /// All subsequent operations on the handle must come from the same
117    /// sender; any other sender gets `EBADF`. Without this check, any
118    /// process that guesses a `file_id` could read driver registers or
119    /// write PCI config across processes.
120    owner: u64,
121}
122
123// === Server ================================================================
124
125pub struct BusSchemeServer {
126    drivers: Vec<(String, Box<dyn BusDriver>)>,
127    port_handle: u64,
128    handles: BTreeMap<u64, OpenHandle>,
129    next_id: u64,
130    pci_cache: Vec<PciDeviceInfo>,
131    /// Rendered `pci/find/<vid>/<did>` results, keyed by (vendor, device).
132    ///
133    /// Each read of a find file used to re-run the `pci_enum` syscall and
134    /// re-render the whole listing; sequential reads of a long result were
135    /// O(reads × devices). The cache is invalidated on every rescan so
136    /// results stay coherent with `pci/inventory`.
137    find_cache: BTreeMap<(u16, u16), Vec<u8>>,
138    /// Driver-name → index into `drivers`, built once at construction.
139    ///
140    /// Path resolution is O(log n) instead of a linear scan over all
141    /// driver names on every open/existence check : significant with the
142    /// 100–1000 drivers this server is expected to host.
143    name_to_idx: BTreeMap<String, usize>,
144}
145
146impl BusSchemeServer {
147    /// Creates a new instance.
148    pub fn new(drivers: Vec<(String, Box<dyn BusDriver>)>, port_handle: u64) -> Self {
149        let name_to_idx = drivers
150            .iter()
151            .enumerate()
152            .map(|(i, (name, _))| (name.clone(), i))
153            .collect();
154        Self {
155            drivers,
156            port_handle,
157            handles: BTreeMap::new(),
158            next_id: 1,
159            pci_cache: Vec::new(),
160            find_cache: BTreeMap::new(),
161            name_to_idx,
162        }
163    }
164
165    // === PCI cache (shared) =================================================
166
167    /// Performs the refresh pci cache operation.
168    ///
169    /// Returns `Ok(count)` with the number of devices found, or `Err(())` if the
170    /// underlying `pci_enum` syscall failed (cache remains unchanged).
171    pub fn refresh_pci_cache(&mut self) -> Result<usize, ()> {
172        let criteria = PciProbeCriteria {
173            match_flags: 0,
174            vendor_id: 0,
175            device_id: 0,
176            class_code: 0,
177            subclass: 0,
178            prog_if: 0,
179            _reserved: 0,
180        };
181        let mut buf = alloc::vec![PciDeviceInfo {
182            address: PciAddress {
183                bus: 0,
184                device: 0,
185                function: 0,
186                _reserved: 0,
187            },
188            vendor_id: 0,
189            device_id: 0,
190            class_code: 0,
191            subclass: 0,
192            prog_if: 0,
193            revision: 0,
194            header_type: 0,
195            interrupt_line: 0,
196            interrupt_pin: 0,
197            _reserved: 0,
198        }; 256];
199        match call::pci_enum(&criteria, &mut buf) {
200            Ok(n) => {
201                let count = n.min(buf.len());
202                self.pci_cache.clear();
203                self.pci_cache.extend_from_slice(&buf[..count]);
204                // Cached find results are stale as soon as the device
205                // list changes.
206                self.find_cache.clear();
207                Ok(self.pci_cache.len())
208            }
209            Err(_) => Err(()),
210        }
211    }
212
213    // === Reply helpers =====================================================
214
215    fn ok_reply(sender: u64) -> IpcMessage {
216        IpcMessage::status_reply(sender, STATUS_OK)
217    }
218
219    fn err_reply(sender: u64, code: usize) -> IpcMessage {
220        IpcMessage::status_reply(sender, code as u32)
221    }
222
223    fn alloc_id(&mut self) -> Option<u64> {
224        let id = self.next_id;
225        // Never wrap: reusing ids after u64::MAX could alias handles that
226        // are still open. Fail cleanly instead.
227        if id == u64::MAX {
228            return None;
229        }
230        self.next_id = id + 1;
231        Some(id)
232    }
233
234    // === Path resolution ===================================================
235
236    /// Count open handles belonging to `sender`.
237    fn count_handles_of(&self, sender: u64) -> usize {
238        self.handles.values().filter(|h| h.owner == sender).count()
239    }
240
241    /// Split a normalised path into a driver index + sub-path, or detect PCI / root.
242    fn resolve_driver_path<'a>(&self, path: &'a str) -> Option<(usize, &'a str)> {
243        let (first, rest) = path.split_once('/').unwrap_or((path, ""));
244        let idx = *self.name_to_idx.get(first)?;
245        Some((idx, rest))
246    }
247
248    fn is_pci_path(path: &str) -> bool {
249        path == PCI_PREFIX || path.starts_with("pci/")
250    }
251
252    // === Path existence ===================================================
253
254    fn path_exists(&self, path: &str) -> bool {
255        // Root is always valid
256        if path.is_empty() {
257            return true;
258        }
259        // PCI paths: strictly validate the sub-path instead of accepting
260        // anything under `pci/` : otherwise opening `/bus/pci/<junk>`
261        // succeeds and burns a handle on a path that can only answer
262        // "unknown".
263        if Self::is_pci_path(path) {
264            return match path {
265                PCI_PREFIX | "pci/inventory" | "pci/count" | "pci/rescan" | "pci/find"
266                | "pci/cfg" => true,
267                p if p.starts_with("pci/find/") => BusSchemeServer::parse_find_path(p).is_some(),
268                p if p.starts_with("pci/cfg/") => BusSchemeServer::parse_cfg_path(p).is_some(),
269                _ => false,
270            };
271        }
272        // Driver paths
273        if let Some((idx, sub)) = self.resolve_driver_path(path) {
274            if sub.is_empty()
275                || sub == DRV_STATUS
276                || sub == DRV_ERROR_COUNT
277                || sub == DRV_SUSPEND
278                || sub == DRV_RESUME
279            {
280                return true;
281            }
282            if sub.starts_with(DRV_REG_PREFIX) {
283                return BusSchemeServer::parse_reg_offset(sub).is_some();
284            }
285            // Firewall capability paths (only when the driver implements it).
286            if sub == DRV_FIREWALL_INFO {
287                return self.drivers[idx].1.as_firewall().is_some();
288            }
289            if sub.starts_with(FIREWALL_GRANT_PREFIX) || sub.starts_with(FIREWALL_RELEASE_PREFIX) {
290                return self.drivers[idx].1.as_firewall().is_some()
291                    && Self::parse_firewall_id(sub).is_some();
292            }
293            // Child device check
294            if self.drivers[idx].1.children().iter().any(|c| c.name == sub) {
295                return true;
296            }
297            return false;
298        }
299        false
300    }
301
302    fn parse_reg_offset(path: &str) -> Option<usize> {
303        let reg_str = path.strip_prefix(DRV_REG_PREFIX)?;
304        BusSchemeServer::parse_hex_usize(reg_str)
305    }
306
307    /// Parse an unsigned hex value, rejecting inputs `from_str_radix`
308    /// would otherwise accept:
309    /// - sign prefixes (`"+1f"`, `"-1f"`),
310    /// - repeated `0x` prefixes (`"0x0x10"`, previously stripped in a loop).
311    fn parse_hex_usize(s: &str) -> Option<usize> {
312        let digits = s.strip_prefix("0x").unwrap_or(s);
313        if digits.is_empty() || digits.starts_with(['+', '-']) {
314            return None;
315        }
316        usize::from_str_radix(digits, 16).ok()
317    }
318
319    fn parse_hex_u8(s: &str) -> Option<u8> {
320        BusSchemeServer::parse_hex_usize(s)?.try_into().ok()
321    }
322
323    fn parse_hex_u16(s: &str) -> Option<u16> {
324        BusSchemeServer::parse_hex_usize(s)?.try_into().ok()
325    }
326
327    /// Parses the peripheral id of a `firewall/<grant|release>/<id>` path.
328    fn parse_firewall_id(sub_path: &str) -> Option<u32> {
329        let id_str = sub_path
330            .strip_prefix(FIREWALL_GRANT_PREFIX)
331            .or_else(|| sub_path.strip_prefix(FIREWALL_RELEASE_PREFIX))?;
332        if id_str.is_empty() {
333            return None;
334        }
335        usize::from_str_radix(id_str.trim_start_matches("0x"), 16)
336            .ok()
337            .and_then(|v| u32::try_from(v).ok())
338    }
339
340    // === Open ================================================================
341
342    fn handle_open(&mut self, sender: u64, payload: &[u8]) -> IpcMessage {
343        // Typed ABI parse: prefix bounds, path bounds and UTF-8 are all
344        // validated in one place instead of hand-rolled offsets here.
345        let (_flags, raw_path) = match OpenRequest::parse(payload) {
346            Some(parsed) => parsed,
347            None => return Self::err_reply(sender, EINVAL),
348        };
349        if raw_path.len() > IpcMessage::OPEN_INLINE_CAPACITY {
350            return Self::err_reply(sender, EINVAL);
351        }
352        let path = match BusSchemeServer::normalize_path(raw_path) {
353            Some(p) => p,
354            // `..` escaping the namespace root.
355            None => return Self::err_reply(sender, EINVAL),
356        };
357        if !self.path_exists(&path) {
358            return Self::err_reply(sender, ENOENT);
359        }
360
361        let file_id = match self.alloc_id() {
362            Some(id) => id,
363            // Handle id space exhausted.
364            None => return Self::err_reply(sender, ENOMEM),
365        };
366        if self.handles.len() >= MAX_OPEN_HANDLES
367            || self.count_handles_of(sender) >= MAX_HANDLES_PER_SENDER
368        {
369            return Self::err_reply(sender, ENOMEM);
370        }
371        let is_dir;
372        let kind = if path.is_empty() {
373            is_dir = true;
374            HandleKind::Root
375        } else if Self::is_pci_path(&path) {
376            // `path.is_empty()` is unreachable here: the Root branch above
377            // already handles it.
378            is_dir = path == PCI_PREFIX || path == "pci/find" || path == "pci/cfg";
379            HandleKind::Pci(path)
380        } else if let Some((idx, sub)) = self.resolve_driver_path(&path) {
381            is_dir = sub.is_empty();
382            HandleKind::Driver {
383                driver_idx: idx,
384                sub_path: String::from(sub),
385            }
386        } else {
387            return Self::err_reply(sender, ENOENT);
388        };
389
390        self.handles.insert(
391            file_id,
392            OpenHandle {
393                kind,
394                owner: sender,
395            },
396        );
397
398        let mut reply = Self::ok_reply(sender);
399        reply.payload[4..12].copy_from_slice(&file_id.to_le_bytes());
400        reply.payload[12..20].copy_from_slice(&0u64.to_le_bytes());
401        reply.payload[20..24]
402            .copy_from_slice(&(if is_dir { IPC_FILE_FLAG_DIRECTORY } else { 0 }).to_le_bytes());
403        reply
404    }
405
406    // === Read ================================================================
407
408    fn handle_read(&mut self, sender: u64, payload: &[u8]) -> IpcMessage {
409        let file_id = u64::from_le_bytes(payload[0..8].try_into().unwrap());
410        let offset = u64::from_le_bytes(payload[8..16].try_into().unwrap());
411
412        // Copy the kind out of the map so the borrow is released before
413        // content generation (which may mutate the find cache).
414        let handle_kind = match self.handles.get(&file_id) {
415            Some(h) if h.owner == sender => Some(h.kind.clone()),
416            _ => None,
417        };
418        let Some(kind) = handle_kind else {
419            return Self::err_reply(sender, EBADF);
420        };
421
422        let content = self.generate_read_content(&kind, offset as usize);
423        // Use the full remaining payload capacity (240 - 8 header bytes),
424        // not an arbitrary cap: each wasted byte costs one extra IPC
425        // round-trip per read.
426        let max = READ_DATA_CAPACITY;
427        let n = content.len().min(max);
428
429        let mut reply = Self::ok_reply(sender);
430        reply.payload[4..8].copy_from_slice(&(n as u32).to_le_bytes());
431        reply.payload[8..8 + n].copy_from_slice(&content[..n]);
432        reply
433    }
434
435    fn generate_read_content(&mut self, kind: &HandleKind, offset: usize) -> Vec<u8> {
436        let data = match kind {
437            HandleKind::Root => {
438                let mut s = format!("drivers registered: {}\n", self.drivers.len());
439                for (name, d) in &self.drivers {
440                    s.push_str(&format!("  {} (compat: {:?})\n", name, d.compatible()));
441                }
442                s.into_bytes()
443            }
444            HandleKind::Pci(path) => self.read_pci_content(path),
445            HandleKind::Driver {
446                driver_idx,
447                sub_path,
448            } => {
449                let driver = &self.drivers[*driver_idx].1;
450                let name = &self.drivers[*driver_idx].0;
451                self.read_driver_content(driver, name, sub_path)
452            }
453        };
454
455        if offset >= data.len() {
456            Vec::new()
457        } else {
458            data[offset..].to_vec()
459        }
460    }
461
462    fn read_pci_content(&mut self, path: &str) -> Vec<u8> {
463        match path {
464            "" | PCI_PREFIX => b"inventory\ncount\nrescan\nfind\ncfg\n".to_vec(),
465            "pci/find" => b"usage: /bus/pci/find/<vendor>/<device>\n".to_vec(),
466            "pci/cfg" => b"usage: /bus/pci/cfg/<bb:dd.f>/<offset>/<width>\n".to_vec(),
467            "pci/inventory" => self.render_inventory(),
468            "pci/count" => format!("{}\n", self.pci_cache.len()).into_bytes(),
469            path if path.starts_with("pci/find/") => {
470                let Some((vendor_id, device_id)) = BusSchemeServer::parse_find_path(path) else {
471                    return b"invalid path\n".to_vec();
472                };
473                // Serve repeated/sequential reads from the cache; the
474                // syscall runs once per (vendor, device) until the next
475                // rescan.
476                if let Some(cached) = self.find_cache.get(&(vendor_id, device_id)) {
477                    return cached.clone();
478                }
479                let criteria = PciProbeCriteria {
480                    match_flags: PCI_MATCH_VENDOR_ID | PCI_MATCH_DEVICE_ID,
481                    vendor_id,
482                    device_id,
483                    class_code: 0,
484                    subclass: 0,
485                    prog_if: 0,
486                    _reserved: 0,
487                };
488                let mut matches = alloc::vec![PciDeviceInfo {
489                    address: PciAddress {
490                        bus: 0,
491                        device: 0,
492                        function: 0,
493                        _reserved: 0,
494                    },
495                    vendor_id: 0,
496                    device_id: 0,
497                    class_code: 0,
498                    subclass: 0,
499                    prog_if: 0,
500                    revision: 0,
501                    header_type: 0,
502                    interrupt_line: 0,
503                    interrupt_pin: 0,
504                    _reserved: 0,
505                }; 64];
506                let rendered = match call::pci_enum(&criteria, &mut matches) {
507                    Ok(n) => {
508                        let mut out = alloc::vec::Vec::new();
509                        for d in matches.into_iter().take(n) {
510                            let line = format!(
511                                "{:02x}:{:02x}.{} {:04x}:{:04x}\n",
512                                d.address.bus,
513                                d.address.device,
514                                d.address.function,
515                                d.vendor_id,
516                                d.device_id
517                            );
518                            out.extend_from_slice(line.as_bytes());
519                        }
520                        if out.is_empty() {
521                            b"none\n".to_vec()
522                        } else {
523                            out
524                        }
525                    }
526                    Err(_) => b"error\n".to_vec(),
527                };
528                // Cache successful queries only: errors and invalid paths
529                // stay uncached so transient failures are retried.
530                if rendered != b"error\n" {
531                    // Bound the cache: evict the smallest key when full.
532                    if self.find_cache.len() >= MAX_FIND_CACHE_ENTRIES {
533                        if let Some(first) = self.find_cache.keys().next().copied() {
534                            self.find_cache.remove(&first);
535                        }
536                    }
537                    self.find_cache
538                        .insert((vendor_id, device_id), rendered.clone());
539                }
540                rendered
541            }
542            path if path.starts_with("pci/cfg/") => {
543                let Some((addr, reg, width)) = BusSchemeServer::parse_cfg_path(path) else {
544                    return b"invalid path\n".to_vec();
545                };
546                match call::pci_cfg_read(&addr, reg, width) {
547                    Ok(v) => format!("0x{:08x}\n", v as u32).into_bytes(),
548                    Err(_) => b"error\n".to_vec(),
549                }
550            }
551            _ => b"unknown\n".to_vec(),
552        }
553    }
554
555    fn read_driver_content(
556        &self,
557        driver: &Box<dyn BusDriver>,
558        name: &str,
559        sub_path: &str,
560    ) -> Vec<u8> {
561        match sub_path {
562            "" => {
563                let mut s = format!("driver: {}\n", name);
564                for c in driver.compatible() {
565                    s.push_str(&format!("compatible: {}\n", c));
566                }
567                s.push_str(&format!("errors: {}\n", driver.error_count()));
568                s.into_bytes()
569            }
570            DRV_STATUS => {
571                format!("driver: {}\nerrors: {}\n", name, driver.error_count()).into_bytes()
572            }
573            DRV_ERROR_COUNT => format!("{}\n", driver.error_count()).into_bytes(),
574            s if s.starts_with(DRV_REG_PREFIX) => {
575                if let Some(reg_offset) = BusSchemeServer::parse_reg_offset(s) {
576                    match driver.read_reg(reg_offset) {
577                        Ok(val) => format!("0x{:08x}\n", val).into_bytes(),
578                        Err(_) => b"error\n".to_vec(),
579                    }
580                } else {
581                    b"invalid register\n".to_vec()
582                }
583            }
584            DRV_FIREWALL_INFO => match driver.as_firewall() {
585                Some(fw) => format!(
586                    "type: {:?}\nmax_entries: {}\n",
587                    fw.firewall_type(),
588                    fw.max_entries()
589                )
590                .into_bytes(),
591                None => b"no firewall capability\n".to_vec(),
592            },
593            child_name => {
594                // Child device info
595                if let Some(child) = driver.children().iter().find(|c| c.name == child_name) {
596                    format!(
597                        "name: {}\nbase: 0x{:x}\nsize: {}\n",
598                        child.name, child.base_addr, child.size
599                    )
600                    .into_bytes()
601                } else {
602                    b"unknown\n".to_vec()
603                }
604            }
605        }
606    }
607
608    // === Write ================================================================
609
610    fn handle_write(&mut self, sender: u64, payload: &[u8]) -> IpcMessage {
611        let file_id = u64::from_le_bytes(payload[0..8].try_into().unwrap());
612        let len = u16::from_le_bytes([payload[16], payload[17]]) as usize;
613
614        let kind = match self.handles.get(&file_id) {
615            Some(h) if h.owner == sender => &h.kind,
616            _ => return Self::err_reply(sender, EBADF),
617        };
618
619        if len > IpcMessage::WRITE_INLINE_CAPACITY {
620            return Self::err_reply(sender, EINVAL);
621        }
622
623        match kind {
624            HandleKind::Pci(path) if *path == "pci/rescan" => {
625                if self.refresh_pci_cache().is_err() {
626                    return Self::err_reply(sender, EIO);
627                }
628            }
629            HandleKind::Pci(path) if path.starts_with("pci/cfg/") => {
630                let Some((addr, reg, width)) = BusSchemeServer::parse_cfg_path(path) else {
631                    return Self::err_reply(sender, EINVAL);
632                };
633                if len < 4 {
634                    return Self::err_reply(sender, EINVAL);
635                }
636                let val = u32::from_le_bytes([payload[18], payload[19], payload[20], payload[21]]);
637                if call::pci_cfg_write(&addr, reg, width, val).is_err() {
638                    return Self::err_reply(sender, EINVAL);
639                }
640            }
641            HandleKind::Driver {
642                driver_idx,
643                sub_path,
644            } if sub_path == DRV_SUSPEND => {
645                if self.drivers[*driver_idx].1.suspend().is_err() {
646                    return Self::err_reply(sender, EIO);
647                }
648            }
649            HandleKind::Driver {
650                driver_idx,
651                sub_path,
652            } if sub_path.starts_with(FIREWALL_GRANT_PREFIX)
653                || sub_path.starts_with(FIREWALL_RELEASE_PREFIX) =>
654            {
655                let grant = sub_path.starts_with(FIREWALL_GRANT_PREFIX);
656                let Some(fw_id) = Self::parse_firewall_id(sub_path) else {
657                    return Self::err_reply(sender, EINVAL);
658                };
659                let Some(fw) = self.drivers[*driver_idx].1.as_firewall() else {
660                    return Self::err_reply(sender, ENOSYS);
661                };
662                let res = if grant {
663                    fw.grant_access(fw_id)
664                } else {
665                    fw.release_access(fw_id)
666                };
667                if res.is_err() {
668                    // InvalidArgument for bad ids, PermissionDenied for
669                    // secure/already-locked peripherals: surface as EINVAL.
670                    return Self::err_reply(sender, EINVAL);
671                }
672            }
673            HandleKind::Driver {
674                driver_idx,
675                sub_path,
676            } if sub_path == DRV_RESUME => {
677                if self.drivers[*driver_idx].1.resume().is_err() {
678                    return Self::err_reply(sender, EIO);
679                }
680            }
681            HandleKind::Driver {
682                driver_idx,
683                sub_path,
684            } if sub_path.starts_with(DRV_REG_PREFIX) => {
685                let Some(reg_offset) = BusSchemeServer::parse_reg_offset(sub_path) else {
686                    return Self::err_reply(sender, EINVAL);
687                };
688                if len < 4 {
689                    return Self::err_reply(sender, EINVAL);
690                }
691                let val = u32::from_le_bytes([payload[18], payload[19], payload[20], payload[21]]);
692                if self.drivers[*driver_idx]
693                    .1
694                    .write_reg(reg_offset, val)
695                    .is_err()
696                {
697                    return Self::err_reply(sender, EINVAL);
698                }
699            }
700            _ => return Self::err_reply(sender, ENOSYS),
701        }
702
703        let mut reply = Self::ok_reply(sender);
704        reply.payload[4..8].copy_from_slice(&(len as u32).to_le_bytes());
705        reply
706    }
707
708    // === Close ================================================================
709
710    fn handle_close(&mut self, sender: u64, payload: &[u8]) -> IpcMessage {
711        let file_id = u64::from_le_bytes(payload[0..8].try_into().unwrap());
712        // Only the owner may close its own handle; a foreign close attempt
713        // must not destroy another client's handle.
714        if matches!(self.handles.get(&file_id), Some(h) if h.owner == sender)
715            && self.handles.remove(&file_id).is_some()
716        {
717            Self::ok_reply(sender)
718        } else {
719            Self::err_reply(sender, EBADF)
720        }
721    }
722
723    // === Read dir ================================================================
724
725    fn handle_readdir(&self, sender: u64, payload: &[u8]) -> IpcMessage {
726        let file_id = u64::from_le_bytes(payload[0..8].try_into().unwrap());
727        let handle = match self.handles.get(&file_id) {
728            Some(h) if h.owner == sender => h,
729            _ => return Self::err_reply(sender, EBADF),
730        };
731
732        let entries: Vec<(u64, u8, String)> = match &handle.kind {
733            HandleKind::Root => {
734                let mut e = alloc::vec![(1u64, DT_DIR, String::from(PCI_PREFIX))];
735                for (i, (name, _)) in self.drivers.iter().enumerate() {
736                    e.push(((i + 2) as u64, DT_DIR, name.clone()));
737                }
738                e
739            }
740            HandleKind::Pci(path) => match path.as_str() {
741                "" | PCI_PREFIX => alloc::vec![
742                    (4u64, DT_REG, String::from("inventory")),
743                    (5u64, DT_REG, String::from("count")),
744                    (6u64, DT_REG, String::from("rescan")),
745                    (7u64, DT_DIR, String::from("find")),
746                    (8u64, DT_DIR, String::from("cfg")),
747                ],
748                "pci/find" | "pci/cfg" => alloc::vec![],
749                _ => return Self::err_reply(sender, ENOTDIR),
750            },
751            HandleKind::Driver {
752                driver_idx,
753                sub_path,
754            } if sub_path.is_empty() => {
755                let driver = &self.drivers[*driver_idx].1;
756                let mut e = alloc::vec![
757                    (1u64, DT_REG, String::from(DRV_STATUS)),
758                    (2u64, DT_REG, String::from(DRV_ERROR_COUNT)),
759                    (3u64, DT_REG, String::from(DRV_SUSPEND)),
760                    (4u64, DT_REG, String::from(DRV_RESUME)),
761                ];
762                if driver.as_firewall().is_some() {
763                    e.push((5u64, DT_DIR, String::from(DRV_FIREWALL_DIR)));
764                }
765                for (i, child) in driver.children().iter().enumerate() {
766                    e.push(((i + 6) as u64, DT_REG, child.name.clone()));
767                }
768                e
769            }
770            HandleKind::Driver {
771                driver_idx,
772                sub_path,
773            } if sub_path == DRV_FIREWALL_DIR => {
774                let _ = self.drivers[*driver_idx].1.as_firewall();
775                alloc::vec![
776                    (1u64, DT_REG, String::from("info")),
777                    (2u64, DT_DIR, String::from("grant")),
778                    (3u64, DT_DIR, String::from("release")),
779                ]
780            }
781            _ => return Self::err_reply(sender, ENOTDIR),
782        };
783
784        let mut reply = Self::ok_reply(sender);
785        let cursor = u16::from_le_bytes([payload[8], payload[9]]) as usize;
786        if cursor >= entries.len() && !entries.is_empty() {
787            reply.payload[4..6].copy_from_slice(&u16::MAX.to_le_bytes());
788            reply.payload[6] = 0;
789            reply.payload[7] = 0;
790            return reply;
791        }
792
793        let mut offset = 8usize;
794        let mut count = 0u8;
795        let mut next_cursor = u16::MAX;
796        let mut index = cursor;
797
798        for (ino, file_type, name) in &entries[cursor..] {
799            let name_bytes = name.as_bytes();
800            let entry_size = 10 + name_bytes.len();
801            // Fill the whole payload (entries start at offset 8).
802            if offset + entry_size > IpcMessage::PAYLOAD_CAPACITY {
803                next_cursor = index.min(u16::MAX as usize) as u16;
804                break;
805            }
806            reply.payload[offset..offset + 8].copy_from_slice(&ino.to_le_bytes());
807            reply.payload[offset + 8] = *file_type;
808            reply.payload[offset + 9] = name_bytes.len() as u8;
809            let end = offset + 10 + name_bytes.len();
810            reply.payload[offset + 10..end].copy_from_slice(name_bytes);
811            offset = end;
812            // With a 240-byte payload and >= 10 bytes per entry, count can
813            // never exceed 24: the u8 field cannot overflow.
814            count += 1;
815            index += 1;
816        }
817
818        reply.payload[4..6].copy_from_slice(&next_cursor.to_le_bytes());
819        reply.payload[6] = count;
820        reply.payload[7] = (offset - READDIR_HEADER_SIZE) as u8;
821        reply
822    }
823
824    // === Serve ================================================================
825
826    /// Dispatches one request and returns the reply message.
827    ///
828    /// Public so that the scheme contract can be exercised by host-side
829    /// integration tests without a live kernel IPC transport.
830    pub fn dispatch(&mut self, msg_type: u32, sender: u64, payload: &[u8]) -> IpcMessage {
831        let mut payload_buf = [0u8; IpcMessage::PAYLOAD_CAPACITY];
832        let n = payload.len().min(payload_buf.len());
833        payload_buf[..n].copy_from_slice(&payload[..n]);
834        match msg_type {
835            OPCODE_OPEN => self.handle_open(sender, &payload_buf),
836            OPCODE_READ => self.handle_read(sender, &payload_buf),
837            OPCODE_WRITE => self.handle_write(sender, &payload_buf),
838            OPCODE_CLOSE => self.handle_close(sender, &payload_buf),
839            OPCODE_READDIR => self.handle_readdir(sender, &payload_buf),
840            _ => Self::err_reply(sender, ENOSYS),
841        }
842    }
843
844    /// Performs the serve operation.
845    pub fn serve(&mut self) -> ! {
846        loop {
847            let mut msg = IpcMessage::new(0);
848            if call::ipc_recv(self.port_handle as usize, &mut msg).is_err() {
849                let _ = call::sched_yield();
850                continue;
851            }
852
853            let reply = self.dispatch(msg.msg_type, msg.sender, &msg.payload);
854            let _ = call::ipc_reply(&reply);
855        }
856    }
857
858    // === Static helpers ========================================================
859
860    /// Normalise a client-supplied path for the `/bus` namespace:
861    /// - collapses repeated `/`,
862    /// - drops `.` segments,
863    /// - resolves `..` lexically (returns `None` if it escapes the root).
864    ///
865    /// The root is the empty string. Without this, a path such as
866    /// `pci/../<driver>/reg/x` could bypass naive prefix matching if
867    /// sub-tree resolution ever becomes recursive.
868    fn normalize_path(path: &str) -> Option<String> {
869        let mut segments: Vec<&str> = Vec::new();
870        for seg in path.split('/') {
871            match seg {
872                "" | "." => {}
873                ".." => {
874                    segments.pop()?;
875                }
876                s => segments.push(s),
877            }
878        }
879        Some(segments.join("/"))
880    }
881
882    fn parse_pci_bdf(s: &str) -> Option<PciAddress> {
883        let (bus_s, rest) = s.split_once(':')?;
884        let (dev_s, fun_s) = rest.split_once('.')?;
885        let bus = BusSchemeServer::parse_hex_u8(bus_s)?;
886        let device = BusSchemeServer::parse_hex_u8(dev_s)?;
887        let function = BusSchemeServer::parse_hex_u8(fun_s)?;
888        if device > 31 || function > 7 {
889            return None;
890        }
891        Some(PciAddress {
892            bus,
893            device,
894            function,
895            _reserved: 0,
896        })
897    }
898
899    fn parse_cfg_path(path: &str) -> Option<(PciAddress, u8, u8)> {
900        let mut parts = path.strip_prefix("pci/cfg/")?.split('/');
901        let bdf = parts.next()?;
902        let off = parts.next()?;
903        let width = parts.next()?;
904        if parts.next().is_some() {
905            return None;
906        }
907        let addr = BusSchemeServer::parse_pci_bdf(bdf)?;
908        let offset = BusSchemeServer::parse_hex_u8(off)?;
909        let width = width.parse::<u8>().ok()?;
910        if !matches!(width, 1 | 2 | 4) {
911            return None;
912        }
913        Some((addr, offset, width))
914    }
915
916    fn parse_find_path(path: &str) -> Option<(u16, u16)> {
917        let mut parts = path.strip_prefix("pci/find/")?.split('/');
918        let ven = BusSchemeServer::parse_hex_u16(parts.next()?)?;
919        let dev = BusSchemeServer::parse_hex_u16(parts.next()?)?;
920        if parts.next().is_some() {
921            return None;
922        }
923        Some((ven, dev))
924    }
925
926    fn render_inventory(&self) -> Vec<u8> {
927        let mut out = alloc::vec::Vec::new();
928        out.extend_from_slice(b"bus:dev.fn vendor:device class:sub prog_if rev irq\n");
929        for d in &self.pci_cache {
930            let line = format!(
931                "{:02x}:{:02x}.{} {:04x}:{:04x} {:02x}:{:02x} {:02x} {:02x} {}\n",
932                d.address.bus,
933                d.address.device,
934                d.address.function,
935                d.vendor_id,
936                d.device_id,
937                d.class_code,
938                d.subclass,
939                d.prog_if,
940                d.revision,
941                d.interrupt_line
942            );
943            out.extend_from_slice(line.as_bytes());
944        }
945        out
946    }
947}
948
949// ===========================================================================
950// Tests : pure path/parsing helpers pinned by the security review.
951// Host-runnable: cargo test -p strat9-bus-drivers
952// ===========================================================================
953
954#[cfg(test)]
955mod review_tests {
956    use super::*;
957
958    // S5 : normalize_path: dot segments, `..` traversal, repeated slashes.
959
960    #[test]
961    fn s5_normalize_collapses_dots_and_slashes() {
962        assert_eq!(
963            BusSchemeServer::normalize_path("pci//find"),
964            Some(String::from("pci/find"))
965        );
966        assert_eq!(
967            BusSchemeServer::normalize_path("./status"),
968            Some(String::from("status"))
969        );
970        assert_eq!(BusSchemeServer::normalize_path("/"), Some(String::new()));
971        assert_eq!(BusSchemeServer::normalize_path(""), Some(String::new()));
972    }
973
974    #[test]
975    fn s5_normalize_resolves_parent_segments_lexically() {
976        // Before the fix, `..` passed through untouched and could bypass
977        // prefix matching.
978        assert_eq!(
979            BusSchemeServer::normalize_path("pci/../nvme0/status"),
980            Some(String::from("nvme0/status"))
981        );
982        assert_eq!(
983            BusSchemeServer::normalize_path("a/b/../c"),
984            Some(String::from("a/c"))
985        );
986    }
987
988    #[test]
989    fn s5_normalize_rejects_root_escaping_traversal() {
990        assert_eq!(BusSchemeServer::normalize_path("../etc/passwd"), None);
991        assert_eq!(BusSchemeServer::normalize_path("pci/../../x"), None);
992    }
993
994    // A4 : hardened hex parsing.
995
996    #[test]
997    fn a4_hex_parsers_reject_sign_and_repeated_prefixes() {
998        assert!(BusSchemeServer::parse_hex_usize("0x10").is_some());
999        assert_eq!(BusSchemeServer::parse_hex_usize("10"), Some(16));
1000        // Previously accepted (trim_start_matches looped):
1001        assert!(BusSchemeServer::parse_hex_usize("0x0x10").is_none());
1002        // Previously accepted (from_str_radix allows '+'):
1003        assert!(BusSchemeServer::parse_hex_usize("+1f").is_none());
1004        assert!(BusSchemeServer::parse_hex_usize("-1f").is_none());
1005        assert!(BusSchemeServer::parse_hex_usize("").is_none());
1006        assert!(BusSchemeServer::parse_hex_u8("100").is_none()); // > u8::MAX
1007        assert!(BusSchemeServer::parse_hex_u16("10000").is_none()); // > u16::MAX
1008    }
1009
1010    #[test]
1011    fn a4_reg_offset_requires_valid_hex() {
1012        assert_eq!(BusSchemeServer::parse_reg_offset("reg/0x1c"), Some(28));
1013        assert!(BusSchemeServer::parse_reg_offset("reg/").is_none());
1014        assert!(BusSchemeServer::parse_reg_offset("reg/+4").is_none());
1015    }
1016
1017    // V3 : strict pci sub-path validation.
1018
1019    #[test]
1020    fn v3_pci_cfg_path_rejects_bad_width_and_bdf() {
1021        let (addr, off, w) = BusSchemeServer::parse_cfg_path("pci/cfg/00:1f.2/10/4").unwrap();
1022        assert_eq!((addr.device, addr.function), (31, 2));
1023        assert_eq!((off, w), (16, 4));
1024        // width must be 1, 2 or 4:
1025        assert!(BusSchemeServer::parse_cfg_path("pci/cfg/00:1f.2/10/3").is_none());
1026        // device > 31 / function > 7 are invalid BDFs:
1027        assert!(BusSchemeServer::parse_cfg_path("pci/cfg/00:20.0/10/4").is_none());
1028        assert!(BusSchemeServer::parse_cfg_path("pci/cfg/00:01.8/10/4").is_none());
1029        // extra segments rejected:
1030        assert!(BusSchemeServer::parse_cfg_path("pci/cfg/00:1f.2/10/4/x").is_none());
1031    }
1032
1033    #[test]
1034    fn v3_pci_find_path_is_strictly_two_fields() {
1035        assert_eq!(BusSchemeServer::parse_find_path("pci/find/8086:100e"), None); // ':' not a separator
1036        assert_eq!(
1037            BusSchemeServer::parse_find_path("pci/find/8086/100e"),
1038            Some((0x8086, 0x100e))
1039        );
1040        assert!(BusSchemeServer::parse_find_path("pci/find/8086").is_none());
1041        assert!(BusSchemeServer::parse_find_path("pci/find/8086/100e/x").is_none());
1042    }
1043
1044    #[test]
1045    fn v3_pci_bdf_bounds_device_and_function() {
1046        assert!(BusSchemeServer::parse_pci_bdf("ff:1f.7").is_some());
1047        assert!(BusSchemeServer::parse_pci_bdf("00:20.0").is_none()); // device 32
1048        assert!(BusSchemeServer::parse_pci_bdf("00:00.8").is_none()); // function 8
1049    }
1050}