syd 3.52.0

rock-solid application kernel
Documentation
// Syd: rock-solid application kernel
// src/ioctl.rs: ioctl(2) request decoder
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::fmt;

use libseccomp::ScmpArch;
use nix::errno::Errno;
use serde::{Serialize, Serializer};

use crate::confine::SCMP_ARCH;

/// This type represents an ioctl(2) request.
pub type Ioctl = u32;

/// This enum represents an ioctl(2) name or value.
pub enum IoctlName {
    /// Request name
    Name(String),
    /// Request value
    Val(Ioctl),
}

impl fmt::Display for IoctlName {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Name(ref s) => write!(f, "{s}"),
            Self::Val(v) => write!(f, "{v:#x}"),
        }
    }
}

impl Serialize for IoctlName {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Self::Name(ref s) => serializer.serialize_str(s),
            Self::Val(v) => serializer.serialize_u32(*v),
        }
    }
}

/// This type represents an ioctl(2) list.
pub type IoctlList = &'static [(&'static str, Ioctl)];

// Include auto-generated ioctl(2) requests.
include!("ioctl/ioctls_aarch64.rs");
include!("ioctl/ioctls_arm.rs");
include!("ioctl/ioctls_loongarch64.rs");
include!("ioctl/ioctls_m68k.rs");
include!("ioctl/ioctls_mips.rs");
include!("ioctl/ioctls_mips64.rs");
include!("ioctl/ioctls_mips64n32.rs");
include!("ioctl/ioctls_mipsel.rs");
include!("ioctl/ioctls_mipsel64.rs");
include!("ioctl/ioctls_mipsel64n32.rs");
include!("ioctl/ioctls_ppc.rs");
include!("ioctl/ioctls_ppc64.rs");
include!("ioctl/ioctls_ppc64le.rs");
include!("ioctl/ioctls_riscv64.rs");
include!("ioctl/ioctls_s390.rs");
include!("ioctl/ioctls_s390x.rs");
include!("ioctl/ioctls_x32.rs");
include!("ioctl/ioctls_x86.rs");
include!("ioctl/ioctls_x8664.rs");

const ARCH_TABLES: &[(ScmpArch, IoctlList)] = &[
    (ScmpArch::Aarch64, IOCTL_ARCH_AARCH64),
    (ScmpArch::Arm, IOCTL_ARCH_ARM),
    (ScmpArch::Loongarch64, IOCTL_ARCH_LOONGARCH64),
    (ScmpArch::M68k, IOCTL_ARCH_M68K),
    (ScmpArch::Mips, IOCTL_ARCH_MIPS),
    (ScmpArch::Mips64, IOCTL_ARCH_MIPS64),
    (ScmpArch::Mips64N32, IOCTL_ARCH_MIPS64N32),
    (ScmpArch::Mipsel, IOCTL_ARCH_MIPSEL),
    (ScmpArch::Mipsel64, IOCTL_ARCH_MIPSEL64),
    (ScmpArch::Mipsel64N32, IOCTL_ARCH_MIPSEL64N32),
    (ScmpArch::Ppc, IOCTL_ARCH_PPC),
    (ScmpArch::Ppc64, IOCTL_ARCH_PPC64),
    (ScmpArch::Ppc64Le, IOCTL_ARCH_PPC64LE),
    (ScmpArch::Riscv64, IOCTL_ARCH_RISCV64),
    (ScmpArch::S390, IOCTL_ARCH_S390),
    (ScmpArch::S390X, IOCTL_ARCH_S390X),
    (ScmpArch::X32, IOCTL_ARCH_X32),
    (ScmpArch::X86, IOCTL_ARCH_X86),
    (ScmpArch::X8664, IOCTL_ARCH_X8664),
];

/// This structure represents ioctl maps.
///
/// It offers an API to query ioctls by name and by value.
/// This implementation uses zero-allocation static lookups.
pub struct IoctlMap {
    target: Option<ScmpArch>,
    native: bool,
}

impl IoctlMap {
    /// Initialize a new IoctlMap.
    ///
    /// The `target` and `native` parameters control which architectures are considered
    /// during lookups, acting as a filter.
    pub fn new(target: Option<ScmpArch>, native: bool) -> Self {
        Self { target, native }
    }

    fn should_check(&self, arch: ScmpArch) -> bool {
        if let Some(target_arch) = self.target {
            if arch != target_arch {
                return false;
            }
        } else if self.native && !SCMP_ARCH.contains(&arch) {
            return false;
        }
        true
    }

    /// Return symbol names for the given Ioctl.
    /// Performs a linear scan of the static table (O(N)).
    pub fn get_names(
        &self,
        value: Ioctl,
        arch: ScmpArch,
    ) -> Result<Option<Vec<&'static str>>, Errno> {
        if !self.should_check(arch) {
            return Ok(None);
        }

        for &(a, table) in ARCH_TABLES {
            if a == arch {
                // Linear scan to find all matches
                let mut names = Vec::new();
                for &(n, v) in table {
                    if Ioctl::from(v) == value {
                        if names.len() == names.capacity() {
                            names.try_reserve(1).or(Err(Errno::ENOMEM))?;
                        }
                        names.push(n);
                    }
                }
                if names.is_empty() {
                    return Ok(None);
                }
                return Ok(Some(names));
            }
        }
        Ok(None)
    }

    /// Return IoctlName list for the given Ioctl.
    /// Uses fallible allocation.
    pub fn get_log(&self, value: Ioctl, arch: ScmpArch) -> Result<Option<Vec<IoctlName>>, Errno> {
        if !self.should_check(arch) {
            return Ok(None);
        }

        for &(a, table) in ARCH_TABLES {
            if a == arch {
                let mut names = Vec::new();
                for &(n, v) in table {
                    if Ioctl::from(v) == value {
                        if names.len() == names.capacity() {
                            names.try_reserve(1).or(Err(Errno::ENOMEM))?;
                        }
                        let mut s = String::new();
                        s.try_reserve(n.len()).or(Err(Errno::ENOMEM))?;
                        s.push_str(n);
                        names.push(IoctlName::Name(s));
                    }
                }
                if names.is_empty() {
                    return Ok(None);
                }
                return Ok(Some(names));
            }
        }
        Ok(None)
    }

    /// Return Ioctl request number for the given symbol name.
    /// Performs a binary search on the static table (O(log N)).
    pub fn get_value(&self, name: &str, arch: ScmpArch) -> Option<Ioctl> {
        if !self.should_check(arch) {
            return None;
        }

        for &(a, table) in ARCH_TABLES {
            if a == arch {
                // Table is sorted by name. Use binary search.
                return table
                    .binary_search_by_key(&name, |&(n, _)| n)
                    .ok()
                    .map(|idx| Ioctl::from(table[idx].1));
            }
        }
        None
    }

    /// Return an iterator over all Ioctls for the given architecture.
    pub fn iter(&self, arch: ScmpArch) -> Option<impl Iterator<Item = (&'static str, Ioctl)>> {
        if !self.should_check(arch) {
            return None;
        }

        for &(a, table) in ARCH_TABLES {
            if a == arch {
                return Some(table.iter().map(|&(name, val)| (name, Ioctl::from(val))));
            }
        }
        None
    }
}

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

    #[test]
    fn test_ioctlmap_1() {
        let map = IoctlMap::new(None, false);
        assert!(map.target.is_none());
        assert!(!map.native);
    }

    #[test]
    fn test_ioctlmap_2() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), true);
        assert_eq!(map.target, Some(ScmpArch::X8664));
        assert!(map.native);
    }

    #[test]
    fn test_ioctlmap_3() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), false);
        let result = map.get_names(0x5413, ScmpArch::X8664).unwrap();
        if let Some(names) = result {
            assert!(names.contains(&"TIOCGWINSZ"));
        }
    }

    #[test]
    fn test_ioctlmap_4() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), false);
        let result = map.get_names(0xDEADBEEF, ScmpArch::X8664).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_ioctlmap_5() {
        let map = IoctlMap::new(Some(ScmpArch::Arm), false);
        let result = map.get_names(0x5413, ScmpArch::X8664).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_ioctlmap_6() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), false);
        let result = map.get_value("TIOCGWINSZ", ScmpArch::X8664);
        assert_eq!(result, Some(0x5413));
    }

    #[test]
    fn test_ioctlmap_7() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), false);
        let result = map.get_value("NONEXISTENT_IOCTL", ScmpArch::X8664);
        assert!(result.is_none());
    }

    #[test]
    fn test_ioctlmap_8() {
        let map = IoctlMap::new(Some(ScmpArch::Arm), false);
        let result = map.get_value("TIOCGWINSZ", ScmpArch::X8664);
        assert!(result.is_none());
    }

    #[test]
    fn test_ioctlmap_9() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), false);
        let result = map.get_log(0x5413, ScmpArch::X8664).unwrap();
        if let Some(names) = result {
            assert!(!names.is_empty());
            let display = format!("{}", names[0]);
            assert!(display.contains("TIOCGWINSZ"));
        }
    }

    #[test]
    fn test_ioctlmap_10() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), false);
        let result = map.get_log(0xDEADBEEF, ScmpArch::X8664).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_ioctlmap_11() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), false);
        let iter = map.iter(ScmpArch::X8664);
        assert!(iter.is_some());
        let count = iter.unwrap().count();
        assert!(count > 0);
    }

    #[test]
    fn test_ioctlmap_12() {
        let map = IoctlMap::new(Some(ScmpArch::Arm), false);
        let iter = map.iter(ScmpArch::X8664);
        assert!(iter.is_none());
    }

    #[test]
    fn test_ioctlname_1() {
        let name = IoctlName::Name("TIOCGWINSZ".into());
        assert_eq!(format!("{name}"), "TIOCGWINSZ");
    }

    #[test]
    fn test_ioctlname_2() {
        let val = IoctlName::Val(0x5413);
        assert_eq!(format!("{val}"), "0x5413");
    }

    #[test]
    fn test_ioctlname_3() {
        let name = IoctlName::Name("TIOCGWINSZ".into());
        let json = serde_json::to_string(&name).unwrap();
        assert_eq!(json, "\"TIOCGWINSZ\"");
    }

    #[test]
    fn test_ioctlname_4() {
        let val = IoctlName::Val(0x5413);
        let json = serde_json::to_string(&val).unwrap();
        assert_eq!(json, "21523");
    }

    #[test]
    fn test_should_check_1() {
        let map = IoctlMap::new(None, false);
        assert!(map.should_check(ScmpArch::X8664));
        assert!(map.should_check(ScmpArch::Arm));
    }

    #[test]
    fn test_should_check_2() {
        let map = IoctlMap::new(Some(ScmpArch::X8664), false);
        assert!(map.should_check(ScmpArch::X8664));
        assert!(!map.should_check(ScmpArch::Arm));
    }
}