1pub mod container;
14pub mod filter;
15pub mod local;
16
17pub use linux_targets::{distro, distros, Distro, KernelVersion};
18
19const SYSCALL_DATA: &str = include_str!("../data/linux/syscalls-x86_64.tsv");
20const MADVISE_DATA: &str = include_str!("../data/linux/madvise-x86_64.tsv");
21
22pub const ENOSYS: i32 = 38;
24pub const EINVAL: i32 = 22;
26pub const PROVIDER_FORMAT: &str = "kernel-abi-provider-v1";
28
29pub const GENERIC_KERNELS: &[&str] = &[
32 "3.10", "3.16", "4.4", "4.9", "4.12", "4.14", "4.15", "4.18", "4.19", "5.3", "5.4", "5.10",
33 "5.14", "5.15", "6.1", "6.4", "6.6", "6.8", "6.12",
34];
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Runner {
39 Local,
42 Container,
45}
46
47impl Runner {
48 pub fn parse(s: &str) -> Result<Self, String> {
50 match s {
51 "local" => Ok(Runner::Local),
52 "container" => Ok(Runner::Container),
53 _ => Err(format!(
54 "unknown runner {s:?} (expected local or container)"
55 )),
56 }
57 }
58
59 pub fn choose(requested: Option<Runner>, image: Option<&str>) -> Result<Self, String> {
64 let image = image.filter(|i| !i.is_empty());
65 match (requested, image) {
66 (Some(Runner::Local), Some(i)) => Err(format!(
67 "the local runner uses the host userland; drop the image {i:?} or use the container runner"
68 )),
69 (Some(Runner::Container), None) => {
70 Err("the container runner needs an image (--image or --distro)".to_string())
71 }
72 (Some(r), _) => Ok(r),
73 (None, Some(_)) => Ok(Runner::Container),
74 (None, None) => Ok(Runner::Local),
75 }
76 }
77}
78
79#[derive(Clone, Debug)]
81pub struct Syscall {
82 pub nr: u32,
83 pub name: &'static str,
84 pub first: KernelVersion,
85}
86
87pub fn syscalls() -> Vec<Syscall> {
89 data_rows(SYSCALL_DATA)
90 .map(|cols| {
91 let [nr, name, first] = cols[..] else {
92 panic!("syscall data row needs 3 columns: {cols:?}");
93 };
94 Syscall {
95 nr: nr.parse().expect("syscall number"),
96 name,
97 first: KernelVersion::parse(first).expect("syscall first_release"),
98 }
99 })
100 .collect()
101}
102
103pub fn table_floor() -> KernelVersion {
106 syscalls()
107 .iter()
108 .map(|s| s.first)
109 .min()
110 .expect("syscall data is empty")
111}
112
113pub fn table_ceiling() -> KernelVersion {
116 SYSCALL_DATA
117 .lines()
118 .find_map(|l| l.strip_prefix("# Tags scanned: "))
119 .and_then(|range| range.split(" .. ").nth(1))
120 .and_then(|last| KernelVersion::parse(last).ok())
121 .expect("syscall data header names the scanned tags")
122}
123
124pub fn missing_syscalls(kernel: KernelVersion, allow: &[&str]) -> Vec<Syscall> {
126 syscalls()
127 .into_iter()
128 .filter(|s| s.first > kernel && !allow.contains(&s.name))
129 .collect()
130}
131
132pub struct Target {
134 pub label: String,
135 pub kernel: KernelVersion,
136 pub allow: Vec<&'static str>,
137}
138
139impl Target {
140 pub fn kernel(kernel: KernelVersion) -> Self {
141 Target {
142 label: format!("Linux {kernel}"),
143 kernel,
144 allow: Vec::new(),
145 }
146 }
147
148 pub fn distro(d: &Distro) -> Self {
149 Target {
150 label: format!("{} (Linux {})", d.name, d.kernel),
151 kernel: d.kernel,
152 allow: d.kernel_backports.clone(),
153 }
154 }
155}
156
157pub fn present_syscalls(target: &Target) -> Vec<Syscall> {
159 syscalls()
160 .into_iter()
161 .filter(|s| s.first <= target.kernel || target.allow.contains(&s.name))
162 .collect()
163}
164
165#[derive(Clone, Debug)]
167pub struct MadviseAdvice {
168 pub value: u32,
169 pub name: &'static str,
170 pub first: KernelVersion,
171}
172
173pub fn madvise_advice() -> Vec<MadviseAdvice> {
175 data_rows(MADVISE_DATA)
176 .map(|cols| {
177 let [value, name, first] = cols[..] else {
178 panic!("madvise data row needs 3 columns: {cols:?}");
179 };
180 MadviseAdvice {
181 value: value.parse().expect("madvise value"),
182 name,
183 first: KernelVersion::parse(first).expect("madvise first_release"),
184 }
185 })
186 .collect()
187}
188
189pub fn madvise_accepts(kernel: KernelVersion, value: u32) -> bool {
192 madvise_advice()
193 .iter()
194 .any(|a| a.value == value && a.first <= kernel)
195}
196
197pub fn seccomp_profile(target: &Target) -> String {
212 let present = present_syscalls(target);
213 let names: Vec<&str> = present
214 .iter()
215 .map(|s| s.name)
216 .filter(|n| *n != "madvise")
217 .collect();
218 let max_advice = madvise_advice().iter().map(|a| a.value).max().unwrap_or(0);
219 let mut entries = vec![name_list_entry(&names)];
220 for value in 0..=max_advice {
221 entries.push(madvise_entry(
222 "SCMP_CMP_MASKED_EQ",
223 u32::MAX as u64,
224 Some(value as u64),
225 madvise_accepts(target.kernel, value),
226 ));
227 }
228 entries.push(madvise_entry("SCMP_CMP_GT", max_advice as u64, None, false));
229
230 let comment = format!(
231 "kernel-abi-tools {PROVIDER_FORMAT}: simulate {} on x86_64; {} syscalls allowed, \
232 all others return ENOSYS; madvise advice it does not know returns EINVAL. \
233 Testing aid only, not a security boundary.",
234 target.label,
235 present.len()
236 );
237 format!(
238 "{{\n \"_comment\": {},\n \"defaultAction\": \"SCMP_ACT_ERRNO\",\n \
239 \"defaultErrnoRet\": {ENOSYS},\n \"architectures\": [\"SCMP_ARCH_X86_64\"],\n \
240 \"syscalls\": [\n{}\n ]\n}}\n",
241 json_str(&comment),
242 entries.join(",\n")
243 )
244}
245
246fn name_list_entry(names: &[&str]) -> String {
247 let list: Vec<String> = names
248 .iter()
249 .map(|n| format!(" {}", json_str(n)))
250 .collect();
251 format!(
252 " {{\n \"names\": [\n{}\n ],\n \"action\": \"SCMP_ACT_ALLOW\"\n }}",
253 list.join(",\n")
254 )
255}
256
257fn madvise_entry(op: &str, value: u64, value_two: Option<u64>, allow: bool) -> String {
258 let action = if allow {
259 "\"action\": \"SCMP_ACT_ALLOW\"".to_string()
260 } else {
261 format!("\"action\": \"SCMP_ACT_ERRNO\", \"errnoRet\": {EINVAL}")
262 };
263 let two = value_two.map_or(String::new(), |v| format!(", \"valueTwo\": {v}"));
264 format!(
265 " {{ \"names\": [\"madvise\"], {action}, \
266 \"args\": [{{ \"index\": 2, \"value\": {value}{two}, \"op\": \"{op}\" }}] }}"
267 )
268}
269
270fn data_rows(data: &'static str) -> impl Iterator<Item = Vec<&'static str>> {
271 data.lines()
272 .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
273 .map(|l| l.split('\t').map(str::trim).collect())
274}
275
276fn json_str(s: &str) -> String {
277 let mut out = String::from("\"");
278 for c in s.chars() {
279 match c {
280 '"' => out.push_str("\\\""),
281 '\\' => out.push_str("\\\\"),
282 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
283 c => out.push(c),
284 }
285 }
286 out.push('"');
287 out
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 fn kv(s: &str) -> KernelVersion {
295 KernelVersion::parse(s).unwrap()
296 }
297
298 #[test]
299 fn parses_distribution_release_strings() {
300 assert_eq!(kv("4.12.14-122.37-default"), kv("4.12"));
301 assert_eq!(kv("v5.14"), kv("5.14.0"));
302 assert!(kv("4.9") < kv("4.12"));
303 assert!(KernelVersion::parse("linux").is_err());
304 }
305
306 #[test]
307 fn known_syscall_introductions() {
308 let first = |n: &str| syscalls().into_iter().find(|s| s.name == n).unwrap().first;
309 assert_eq!(first("getrandom"), kv("3.17"));
310 assert_eq!(first("copy_file_range"), kv("4.5"));
311 assert_eq!(first("statx"), kv("4.11"));
312 assert_eq!(first("clone3"), kv("5.3"));
313 assert_eq!(first("close_range"), kv("5.9"));
314 assert_eq!(table_floor(), kv("3.3"));
315 assert_eq!(table_ceiling(), kv("7.2"));
317 }
318
319 #[test]
320 fn linux_4_12_profile_boundaries() {
321 let names: Vec<_> = missing_syscalls(kv("4.12"), &[])
322 .iter()
323 .map(|s| s.name)
324 .collect();
325 assert!(names.contains(&"rseq"));
326 assert!(names.contains(&"io_uring_setup"));
327 assert!(names.contains(&"clone3"));
328 assert!(!names.contains(&"statx"));
329 assert!(!names.contains(&"copy_file_range"));
330 assert!(!names.contains(&"read"));
331 }
332
333 #[test]
334 fn backports_are_not_blocked() {
335 let names: Vec<_> = missing_syscalls(kv("3.10"), &["getrandom"])
336 .iter()
337 .map(|s| s.name)
338 .collect();
339 assert!(!names.contains(&"getrandom"));
340 assert!(names.contains(&"memfd_create"));
341 }
342
343 #[test]
344 fn profile_is_a_fail_closed_allowlist() {
345 let p = seccomp_profile(&Target::kernel(kv("4.12")));
346 assert!(p.contains("\"defaultAction\": \"SCMP_ACT_ERRNO\""));
347 assert!(p.contains("\"defaultErrnoRet\": 38"));
348 assert!(p.contains("\"statx\""));
349 assert!(!p.contains("\"rseq\""));
350 let all = seccomp_profile(&Target::kernel(table_ceiling()));
351 assert_eq!(all.matches("\n \"").count(), syscalls().len() - 1);
353 }
354
355 #[test]
356 fn madvise_advice_introductions() {
357 let first = |n: &str| {
358 madvise_advice()
359 .into_iter()
360 .find(|a| a.name == n)
361 .unwrap()
362 .first
363 };
364 assert_eq!(first("MADV_FREE"), kv("4.5"));
365 assert_eq!(first("MADV_WIPEONFORK"), kv("4.14"));
366 assert_eq!(first("MADV_POPULATE_READ"), kv("5.14"));
367 assert!(madvise_accepts(kv("4.12"), 4)); assert!(madvise_accepts(kv("4.12"), 8)); assert!(!madvise_accepts(kv("4.12"), 22)); assert!(!madvise_accepts(table_ceiling(), 5)); }
372
373 #[test]
374 fn madvise_rules_cover_every_value_once() {
375 let p = seccomp_profile(&Target::kernel(kv("4.12")));
376 let max = madvise_advice().iter().map(|a| a.value).max().unwrap();
377 let rules = p.matches("\"names\": [\"madvise\"]").count();
378 assert_eq!(rules as u32, max + 2); assert!(p.contains(
380 "\"action\": \"SCMP_ACT_ERRNO\", \"errnoRet\": 22, \
381 \"args\": [{ \"index\": 2, \"value\": 4294967295, \"valueTwo\": 22,"
382 ));
383 assert!(p.contains(
384 "\"action\": \"SCMP_ACT_ALLOW\", \
385 \"args\": [{ \"index\": 2, \"value\": 4294967295, \"valueTwo\": 8,"
386 ));
387 }
388
389 #[test]
390 fn runner_selection_rules() {
391 use Runner::*;
392 assert_eq!(Runner::choose(None, None), Ok(Local));
393 assert_eq!(Runner::choose(None, Some("img")), Ok(Container));
394 assert_eq!(Runner::choose(None, Some("")), Ok(Local));
395 assert_eq!(Runner::choose(Some(Local), None), Ok(Local));
396 assert_eq!(Runner::choose(Some(Container), Some("img")), Ok(Container));
397 assert!(Runner::choose(Some(Local), Some("img")).is_err());
398 assert!(Runner::choose(Some(Container), None).is_err());
399 assert!(Runner::parse("docker").is_err());
400 }
401
402 #[test]
403 fn every_distro_preset_parses() {
404 let all = distros();
405 assert!(all
406 .iter()
407 .any(|d| d.id == "sles-12-sp5" && d.kernel == kv("4.12")));
408 for d in &all {
409 for b in &d.kernel_backports {
410 assert!(
411 syscalls().iter().any(|s| s.name == *b),
412 "{}: unknown backport {b}",
413 d.id
414 );
415 }
416 }
417 }
418}