|
| 1 | +//! POSIX syscall classification table for `libc` foreign function calls. |
| 2 | +//! |
| 3 | +//! Maps common `libc` function names to ambient authority categories. |
| 4 | +//! `libc` is on the driver's skip list, so the syntactic scanner never sees it — |
| 5 | +//! this table is the only way these calls get classified beyond generic FFI. |
| 6 | +
|
| 7 | +use std::collections::HashMap; |
| 8 | +use std::sync::LazyLock; |
| 9 | + |
| 10 | +pub struct LibcClassification { |
| 11 | + pub category: &'static str, |
| 12 | + pub subcategory: &'static str, |
| 13 | + pub risk: &'static str, |
| 14 | +} |
| 15 | + |
| 16 | +static LIBC_TABLE: LazyLock<HashMap<&'static str, LibcClassification>> = |
| 17 | + LazyLock::new(build_table); |
| 18 | + |
| 19 | +pub fn classify_libc_fn(fn_name: &str) -> Option<&'static LibcClassification> { |
| 20 | + LIBC_TABLE.get(fn_name) |
| 21 | +} |
| 22 | + |
| 23 | +fn build_table() -> HashMap<&'static str, LibcClassification> { |
| 24 | + let mut m = HashMap::with_capacity(80); |
| 25 | + |
| 26 | + // ── Filesystem read ── |
| 27 | + for name in [ |
| 28 | + "open", "open64", "openat", "openat64", "creat", "creat64", |
| 29 | + "read", "pread", "pread64", "readv", |
| 30 | + "readlink", "readlinkat", |
| 31 | + "stat", "stat64", "fstat", "fstat64", "lstat", "lstat64", |
| 32 | + "fstatat", "fstatat64", |
| 33 | + "access", "faccessat", |
| 34 | + "readdir", "readdir_r", "fdopendir", "opendir", |
| 35 | + "getdents", "getdents64", |
| 36 | + "realpath", |
| 37 | + ] { |
| 38 | + m.insert(name, LibcClassification { |
| 39 | + category: "Fs", |
| 40 | + subcategory: "read", |
| 41 | + risk: "Medium", |
| 42 | + }); |
| 43 | + } |
| 44 | + |
| 45 | + // ── Filesystem write ── |
| 46 | + for name in [ |
| 47 | + "write", "pwrite", "pwrite64", "writev", |
| 48 | + "unlink", "unlinkat", "remove", |
| 49 | + "rename", "renameat", "renameat2", |
| 50 | + "mkdir", "mkdirat", "rmdir", |
| 51 | + "truncate", "ftruncate", "ftruncate64", |
| 52 | + "chmod", "fchmod", "fchmodat", |
| 53 | + "chown", "fchown", "lchown", "fchownat", |
| 54 | + "link", "linkat", "symlink", "symlinkat", |
| 55 | + "close", "fsync", "fdatasync", |
| 56 | + ] { |
| 57 | + m.insert(name, LibcClassification { |
| 58 | + category: "Fs", |
| 59 | + subcategory: "write", |
| 60 | + risk: "High", |
| 61 | + }); |
| 62 | + } |
| 63 | + |
| 64 | + // ── Filesystem memory-mapped I/O ── |
| 65 | + for name in ["mmap", "mmap64", "munmap", "mprotect", "msync"] { |
| 66 | + m.insert(name, LibcClassification { |
| 67 | + category: "Fs", |
| 68 | + subcategory: "mmap", |
| 69 | + risk: "High", |
| 70 | + }); |
| 71 | + } |
| 72 | + |
| 73 | + // ── Network ── |
| 74 | + for name in [ |
| 75 | + "socket", "connect", "bind", "listen", |
| 76 | + "accept", "accept4", |
| 77 | + "recv", "recvfrom", "recvmsg", |
| 78 | + "send", "sendto", "sendmsg", |
| 79 | + "shutdown", |
| 80 | + "getaddrinfo", "freeaddrinfo", "getnameinfo", |
| 81 | + "getsockname", "getpeername", |
| 82 | + "setsockopt", "getsockopt", |
| 83 | + "poll", "ppoll", |
| 84 | + "select", "pselect", |
| 85 | + "epoll_create", "epoll_create1", "epoll_ctl", "epoll_wait", "epoll_pwait", |
| 86 | + "kqueue", "kevent", |
| 87 | + ] { |
| 88 | + m.insert(name, LibcClassification { |
| 89 | + category: "Net", |
| 90 | + subcategory: "socket", |
| 91 | + risk: "High", |
| 92 | + }); |
| 93 | + } |
| 94 | + |
| 95 | + // ── Environment read ── |
| 96 | + for name in ["getenv", "getcwd", "getuid", "geteuid", "getgid", "getegid", "getpid", "getppid"] { |
| 97 | + m.insert(name, LibcClassification { |
| 98 | + category: "Env", |
| 99 | + subcategory: "read", |
| 100 | + risk: "Medium", |
| 101 | + }); |
| 102 | + } |
| 103 | + |
| 104 | + // ── Environment write ── |
| 105 | + for name in ["setenv", "unsetenv", "putenv", "chdir", "fchdir"] { |
| 106 | + m.insert(name, LibcClassification { |
| 107 | + category: "Env", |
| 108 | + subcategory: "write", |
| 109 | + risk: "High", |
| 110 | + }); |
| 111 | + } |
| 112 | + |
| 113 | + // ── Process ── |
| 114 | + for name in [ |
| 115 | + "fork", "vfork", |
| 116 | + "execve", "execvp", "execvpe", "fexecve", "execl", "execlp", |
| 117 | + "system", |
| 118 | + "posix_spawn", "posix_spawnp", |
| 119 | + "kill", "raise", |
| 120 | + "waitpid", "wait4", "wait", |
| 121 | + "ptrace", |
| 122 | + "exit", "_exit", |
| 123 | + ] { |
| 124 | + m.insert(name, LibcClassification { |
| 125 | + category: "Process", |
| 126 | + subcategory: "spawn", |
| 127 | + risk: "Critical", |
| 128 | + }); |
| 129 | + } |
| 130 | + |
| 131 | + // ── Signal handling (process control) ── |
| 132 | + for name in ["signal", "sigaction", "sigprocmask", "sigsuspend"] { |
| 133 | + m.insert(name, LibcClassification { |
| 134 | + category: "Process", |
| 135 | + subcategory: "signal", |
| 136 | + risk: "High", |
| 137 | + }); |
| 138 | + } |
| 139 | + |
| 140 | + m |
| 141 | +} |
| 142 | + |
| 143 | +#[cfg(test)] |
| 144 | +mod tests { |
| 145 | + use super::*; |
| 146 | + |
| 147 | + #[test] |
| 148 | + fn filesystem_calls_classified() { |
| 149 | + for name in ["open", "read", "stat", "readlink", "access"] { |
| 150 | + let c = classify_libc_fn(name) |
| 151 | + .unwrap_or_else(|| panic!("{name} should be classified")); |
| 152 | + assert_eq!(c.category, "Fs", "{name} should be Fs"); |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + #[test] |
| 157 | + fn network_calls_classified() { |
| 158 | + for name in ["socket", "connect", "bind", "accept", "send"] { |
| 159 | + let c = classify_libc_fn(name) |
| 160 | + .unwrap_or_else(|| panic!("{name} should be classified")); |
| 161 | + assert_eq!(c.category, "Net", "{name} should be Net"); |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + #[test] |
| 166 | + fn env_calls_classified() { |
| 167 | + for name in ["getenv", "getcwd", "setenv", "chdir"] { |
| 168 | + let c = classify_libc_fn(name) |
| 169 | + .unwrap_or_else(|| panic!("{name} should be classified")); |
| 170 | + assert_eq!(c.category, "Env", "{name} should be Env"); |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + #[test] |
| 175 | + fn process_calls_classified() { |
| 176 | + for name in ["fork", "execve", "kill", "waitpid", "system"] { |
| 177 | + let c = classify_libc_fn(name) |
| 178 | + .unwrap_or_else(|| panic!("{name} should be classified")); |
| 179 | + assert_eq!(c.category, "Process", "{name} should be Process"); |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + #[test] |
| 184 | + fn unknown_fn_returns_none() { |
| 185 | + assert!(classify_libc_fn("some_random_fn").is_none()); |
| 186 | + } |
| 187 | + |
| 188 | + #[test] |
| 189 | + fn table_size_sanity() { |
| 190 | + let table = build_table(); |
| 191 | + assert!(table.len() >= 60, "Table too small: {}", table.len()); |
| 192 | + assert!(table.len() <= 120, "Table too large: {}", table.len()); |
| 193 | + } |
| 194 | +} |
0 commit comments