1#![warn(missing_docs)]
35
36pub mod container;
37pub mod filter;
38pub mod local;
39
40pub use linux_targets::{distro, distros, Distro, KernelVersion};
41
42const SYSCALL_DATA: &str = include_str!("../data/linux/syscalls-x86_64.tsv");
43const MADVISE_DATA: &str = include_str!("../data/linux/madvise-x86_64.tsv");
44
45pub const ENOSYS: i32 = 38;
47pub const EINVAL: i32 = 22;
49pub const PROVIDER_FORMAT: &str = "kernel-abi-provider-v1";
51
52pub const GENERIC_KERNELS: &[&str] = &[
55 "3.10", "3.16", "4.4", "4.9", "4.12", "4.14", "4.15", "4.18", "4.19", "5.3", "5.4", "5.10",
56 "5.14", "5.15", "6.1", "6.4", "6.6", "6.8", "6.12",
57];
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum Runner {
62 Local,
65 Container,
68}
69
70impl Runner {
71 pub fn parse(s: &str) -> Result<Self, String> {
77 match s {
78 "local" => Ok(Runner::Local),
79 "container" => Ok(Runner::Container),
80 _ => Err(format!(
81 "unknown runner {s:?} (expected local or container)"
82 )),
83 }
84 }
85
86 pub fn choose(requested: Option<Runner>, image: Option<&str>) -> Result<Self, String> {
96 let image = image.filter(|i| !i.is_empty());
97 match (requested, image) {
98 (Some(Runner::Local), Some(i)) => Err(format!(
99 "the local runner uses the host userland; drop the image {i:?} or use the container runner"
100 )),
101 (Some(Runner::Container), None) => {
102 Err("the container runner needs an image (--image or --distro)".to_string())
103 }
104 (Some(r), _) => Ok(r),
105 (None, Some(_)) => Ok(Runner::Container),
106 (None, None) => Ok(Runner::Local),
107 }
108 }
109}
110
111#[derive(Clone, Debug)]
113pub struct Syscall {
114 pub nr: u32,
116 pub name: &'static str,
118 pub first: KernelVersion,
121}
122
123pub fn syscalls() -> Vec<Syscall> {
125 data_rows(SYSCALL_DATA)
126 .map(|cols| {
127 let [nr, name, first] = cols[..] else {
128 panic!("syscall data row needs 3 columns: {cols:?}");
129 };
130 Syscall {
131 nr: nr.parse().expect("syscall number"),
132 name,
133 first: KernelVersion::parse(first).expect("syscall first_release"),
134 }
135 })
136 .collect()
137}
138
139pub fn table_floor() -> KernelVersion {
142 syscalls()
143 .iter()
144 .map(|s| s.first)
145 .min()
146 .expect("syscall data is empty")
147}
148
149pub fn table_ceiling() -> KernelVersion {
152 SYSCALL_DATA
153 .lines()
154 .find_map(|l| l.strip_prefix("# Tags scanned: "))
155 .and_then(|range| range.split(" .. ").nth(1))
156 .and_then(|last| KernelVersion::parse(last).ok())
157 .expect("syscall data header names the scanned tags")
158}
159
160pub fn missing_syscalls(kernel: KernelVersion, allow: &[&str]) -> Vec<Syscall> {
162 syscalls()
163 .into_iter()
164 .filter(|s| s.first > kernel && !allow.contains(&s.name))
165 .collect()
166}
167
168pub struct Target {
170 pub label: String,
172 pub kernel: KernelVersion,
174 pub allow: Vec<&'static str>,
176}
177
178impl Target {
179 pub fn kernel(kernel: KernelVersion) -> Self {
181 Target {
182 label: format!("Linux {kernel}"),
183 kernel,
184 allow: Vec::new(),
185 }
186 }
187
188 pub fn distro(d: &Distro) -> Self {
190 Target {
191 label: format!("{} (Linux {})", d.name, d.kernel),
192 kernel: d.kernel,
193 allow: d.kernel_backports.clone(),
194 }
195 }
196}
197
198pub fn present_syscalls(target: &Target) -> Vec<Syscall> {
200 syscalls()
201 .into_iter()
202 .filter(|s| s.first <= target.kernel || target.allow.contains(&s.name))
203 .collect()
204}
205
206#[derive(Clone, Debug)]
208pub struct MadviseAdvice {
209 pub value: u32,
211 pub name: &'static str,
213 pub first: KernelVersion,
216}
217
218pub fn madvise_advice() -> Vec<MadviseAdvice> {
220 data_rows(MADVISE_DATA)
221 .map(|cols| {
222 let [value, name, first] = cols[..] else {
223 panic!("madvise data row needs 3 columns: {cols:?}");
224 };
225 MadviseAdvice {
226 value: value.parse().expect("madvise value"),
227 name,
228 first: KernelVersion::parse(first).expect("madvise first_release"),
229 }
230 })
231 .collect()
232}
233
234pub fn madvise_accepts(kernel: KernelVersion, value: u32) -> bool {
237 madvise_advice()
238 .iter()
239 .any(|a| a.value == value && a.first <= kernel)
240}
241
242pub fn seccomp_profile(target: &Target) -> String {
257 let present = present_syscalls(target);
258 let names: Vec<&str> = present
259 .iter()
260 .map(|s| s.name)
261 .filter(|n| *n != "madvise")
262 .collect();
263 let max_advice = madvise_advice().iter().map(|a| a.value).max().unwrap_or(0);
264 let mut entries = vec![name_list_entry(&names)];
265 for value in 0..=max_advice {
266 entries.push(madvise_entry(
267 "SCMP_CMP_MASKED_EQ",
268 u32::MAX as u64,
269 Some(value as u64),
270 madvise_accepts(target.kernel, value),
271 ));
272 }
273 entries.push(madvise_entry("SCMP_CMP_GT", max_advice as u64, None, false));
274
275 let comment = format!(
276 "kernel-abi-tools {PROVIDER_FORMAT}: simulate {} on x86_64; {} syscalls allowed, \
277 all others return ENOSYS; madvise advice it does not know returns EINVAL. \
278 Testing aid only, not a security boundary.",
279 target.label,
280 present.len()
281 );
282 format!(
283 "{{\n \"_comment\": {},\n \"defaultAction\": \"SCMP_ACT_ERRNO\",\n \
284 \"defaultErrnoRet\": {ENOSYS},\n \"architectures\": [\"SCMP_ARCH_X86_64\"],\n \
285 \"syscalls\": [\n{}\n ]\n}}\n",
286 json_str(&comment),
287 entries.join(",\n")
288 )
289}
290
291fn name_list_entry(names: &[&str]) -> String {
292 let list: Vec<String> = names
293 .iter()
294 .map(|n| format!(" {}", json_str(n)))
295 .collect();
296 format!(
297 " {{\n \"names\": [\n{}\n ],\n \"action\": \"SCMP_ACT_ALLOW\"\n }}",
298 list.join(",\n")
299 )
300}
301
302fn madvise_entry(op: &str, value: u64, value_two: Option<u64>, allow: bool) -> String {
303 let action = if allow {
304 "\"action\": \"SCMP_ACT_ALLOW\"".to_string()
305 } else {
306 format!("\"action\": \"SCMP_ACT_ERRNO\", \"errnoRet\": {EINVAL}")
307 };
308 let two = value_two.map_or(String::new(), |v| format!(", \"valueTwo\": {v}"));
309 format!(
310 " {{ \"names\": [\"madvise\"], {action}, \
311 \"args\": [{{ \"index\": 2, \"value\": {value}{two}, \"op\": \"{op}\" }}] }}"
312 )
313}
314
315fn data_rows(data: &'static str) -> impl Iterator<Item = Vec<&'static str>> {
316 data.lines()
317 .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
318 .map(|l| l.split('\t').map(str::trim).collect())
319}
320
321fn json_str(s: &str) -> String {
322 let mut out = String::from("\"");
323 for c in s.chars() {
324 match c {
325 '"' => out.push_str("\\\""),
326 '\\' => out.push_str("\\\\"),
327 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
328 c => out.push(c),
329 }
330 }
331 out.push('"');
332 out
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 fn kv(s: &str) -> KernelVersion {
340 KernelVersion::parse(s).unwrap()
341 }
342
343 #[test]
344 fn parses_distribution_release_strings() {
345 assert_eq!(kv("4.12.14-122.37-default"), kv("4.12"));
346 assert_eq!(kv("v5.14"), kv("5.14.0"));
347 assert!(kv("4.9") < kv("4.12"));
348 assert!(KernelVersion::parse("linux").is_err());
349 }
350
351 #[test]
352 fn known_syscall_introductions() {
353 let first = |n: &str| syscalls().into_iter().find(|s| s.name == n).unwrap().first;
354 assert_eq!(first("getrandom"), kv("3.17"));
355 assert_eq!(first("copy_file_range"), kv("4.5"));
356 assert_eq!(first("statx"), kv("4.11"));
357 assert_eq!(first("clone3"), kv("5.3"));
358 assert_eq!(first("close_range"), kv("5.9"));
359 assert_eq!(table_floor(), kv("3.3"));
360 assert_eq!(table_ceiling(), kv("7.2"));
362 }
363
364 #[test]
365 fn linux_4_12_profile_boundaries() {
366 let names: Vec<_> = missing_syscalls(kv("4.12"), &[])
367 .iter()
368 .map(|s| s.name)
369 .collect();
370 assert!(names.contains(&"rseq"));
371 assert!(names.contains(&"io_uring_setup"));
372 assert!(names.contains(&"clone3"));
373 assert!(!names.contains(&"statx"));
374 assert!(!names.contains(&"copy_file_range"));
375 assert!(!names.contains(&"read"));
376 }
377
378 #[test]
379 fn backports_are_not_blocked() {
380 let names: Vec<_> = missing_syscalls(kv("3.10"), &["getrandom"])
381 .iter()
382 .map(|s| s.name)
383 .collect();
384 assert!(!names.contains(&"getrandom"));
385 assert!(names.contains(&"memfd_create"));
386 }
387
388 #[test]
389 fn profile_is_a_fail_closed_allowlist() {
390 let p = seccomp_profile(&Target::kernel(kv("4.12")));
391 assert!(p.contains("\"defaultAction\": \"SCMP_ACT_ERRNO\""));
392 assert!(p.contains("\"defaultErrnoRet\": 38"));
393 assert!(p.contains("\"statx\""));
394 assert!(!p.contains("\"rseq\""));
395 let all = seccomp_profile(&Target::kernel(table_ceiling()));
396 assert_eq!(all.matches("\n \"").count(), syscalls().len() - 1);
398 }
399
400 #[test]
401 fn madvise_advice_introductions() {
402 let first = |n: &str| {
403 madvise_advice()
404 .into_iter()
405 .find(|a| a.name == n)
406 .unwrap()
407 .first
408 };
409 assert_eq!(first("MADV_FREE"), kv("4.5"));
410 assert_eq!(first("MADV_WIPEONFORK"), kv("4.14"));
411 assert_eq!(first("MADV_POPULATE_READ"), kv("5.14"));
412 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)); }
417
418 #[test]
419 fn madvise_rules_cover_every_value_once() {
420 let p = seccomp_profile(&Target::kernel(kv("4.12")));
421 let max = madvise_advice().iter().map(|a| a.value).max().unwrap();
422 let rules = p.matches("\"names\": [\"madvise\"]").count();
423 assert_eq!(rules as u32, max + 2); assert!(p.contains(
425 "\"action\": \"SCMP_ACT_ERRNO\", \"errnoRet\": 22, \
426 \"args\": [{ \"index\": 2, \"value\": 4294967295, \"valueTwo\": 22,"
427 ));
428 assert!(p.contains(
429 "\"action\": \"SCMP_ACT_ALLOW\", \
430 \"args\": [{ \"index\": 2, \"value\": 4294967295, \"valueTwo\": 8,"
431 ));
432 }
433
434 #[test]
435 fn runner_selection_rules() {
436 use Runner::*;
437 assert_eq!(Runner::choose(None, None), Ok(Local));
438 assert_eq!(Runner::choose(None, Some("img")), Ok(Container));
439 assert_eq!(Runner::choose(None, Some("")), Ok(Local));
440 assert_eq!(Runner::choose(Some(Local), None), Ok(Local));
441 assert_eq!(Runner::choose(Some(Container), Some("img")), Ok(Container));
442 assert!(Runner::choose(Some(Local), Some("img")).is_err());
443 assert!(Runner::choose(Some(Container), None).is_err());
444 assert!(Runner::parse("docker").is_err());
445 }
446
447 #[test]
448 fn every_distro_preset_parses() {
449 let all = distros();
450 assert!(all
451 .iter()
452 .any(|d| d.id == "sles-12-sp5" && d.kernel == kv("4.12")));
453 for d in &all {
454 for b in &d.kernel_backports {
455 assert!(
456 syscalls().iter().any(|s| s.name == *b),
457 "{}: unknown backport {b}",
458 d.id
459 );
460 }
461 }
462 }
463}