1#![allow(non_snake_case)]
10
11pub use libc::sock_filter;
12use syscalls::Errno;
13use syscalls::Sysno;
14
15use crate::fd::Fd;
16
17pub const BPF_LD: u16 = 0x00;
21pub const BPF_ST: u16 = 0x02;
22pub const BPF_JMP: u16 = 0x05;
23pub const BPF_RET: u16 = 0x06;
24
25pub const BPF_W: u16 = 0x00;
27
28pub const BPF_ABS: u16 = 0x20;
29pub const BPF_MEM: u16 = 0x60;
30
31pub const BPF_JEQ: u16 = 0x10;
32pub const BPF_JGT: u16 = 0x20;
33pub const BPF_JGE: u16 = 0x30;
34pub const BPF_K: u16 = 0x00;
35
36pub const BPF_MAXINSNS: usize = 4096;
38
39const SECCOMP_SET_MODE_FILTER: u32 = 1;
41
42const SECCOMP_DATA_OFFSET_NR: u32 = 0;
44
45const SECCOMP_DATA_OFFSET_ARCH: u32 = 4;
47
48const SECCOMP_DATA_OFFSET_IP: u32 = 8;
50
51#[allow(unused)]
53const SECCOMP_DATA_OFFSET_ARGS: u32 = 16;
54
55#[cfg(target_endian = "little")]
56const SECCOMP_DATA_OFFSET_IP_HI: u32 = SECCOMP_DATA_OFFSET_IP + 4;
57#[cfg(target_endian = "little")]
58const SECCOMP_DATA_OFFSET_IP_LO: u32 = SECCOMP_DATA_OFFSET_IP;
59
60#[cfg(target_endian = "big")]
61const SECCOMP_DATA_OFFSET_IP_HI: u32 = SECCOMP_DATA_OFFSET_IP;
62#[cfg(target_endian = "big")]
63const SECCOMP_DATA_OFFSET_IP_LO: u32 = SECCOMP_DATA_OFFSET_IP + 4;
64
65const EM_386: u32 = 3;
67const EM_MIPS: u32 = 8;
68const EM_PPC: u32 = 20;
69const EM_PPC64: u32 = 21;
70const EM_ARM: u32 = 40;
71const EM_X86_64: u32 = 62;
72const EM_AARCH64: u32 = 183;
73
74const __AUDIT_ARCH_64BIT: u32 = 0x8000_0000;
76const __AUDIT_ARCH_LE: u32 = 0x4000_0000;
77
78pub const AUDIT_ARCH_X86: u32 = EM_386 | __AUDIT_ARCH_LE;
80pub const AUDIT_ARCH_X86_64: u32 = EM_X86_64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE;
81pub const AUDIT_ARCH_ARM: u32 = EM_ARM | __AUDIT_ARCH_LE;
82pub const AUDIT_ARCH_AARCH64: u32 = EM_AARCH64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE;
83pub const AUDIT_ARCH_MIPS: u32 = EM_MIPS;
84pub const AUDIT_ARCH_PPC: u32 = EM_PPC;
85pub const AUDIT_ARCH_PPC64: u32 = EM_PPC64 | __AUDIT_ARCH_64BIT;
86
87bitflags::bitflags! {
88 #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
89 struct FilterFlags: u32 {
90 const TSYNC = 1 << 0;
91 const LOG = 1 << 1;
92 const SPEC_ALLOW = 1 << 2;
93 const NEW_LISTENER = 1 << 3;
94 const TSYNC_ESRCH = 1 << 4;
95 }
96}
97
98#[derive(Debug, Clone, Eq, PartialEq)]
100pub struct Filter {
101 filter: Vec<sock_filter>,
107}
108
109impl Default for Filter {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl Filter {
116 pub const fn new() -> Self {
119 Self { filter: Vec::new() }
120 }
121
122 pub fn push(&mut self, instruction: sock_filter) {
124 self.filter.push(instruction);
125 }
126
127 pub fn len(&self) -> usize {
129 self.filter.len()
130 }
131
132 pub fn is_empty(&self) -> bool {
135 self.filter.is_empty()
136 }
137
138 pub fn instructions(&self) -> &[sock_filter] {
141 &self.filter
142 }
143
144 fn install(&self, flags: FilterFlags) -> Result<i32, Errno> {
145 let len = self.filter.len();
146
147 if len == 0 || len > BPF_MAXINSNS {
148 return Err(Errno::EINVAL);
149 }
150
151 let prog = libc::sock_fprog {
152 len: len as u16,
155 filter: self.filter.as_ptr() as *mut _,
156 };
157
158 let ptr = &prog as *const libc::sock_fprog;
159
160 let value = Errno::result(unsafe {
161 libc::syscall(
162 libc::SYS_seccomp,
163 SECCOMP_SET_MODE_FILTER,
164 flags.bits(),
165 ptr,
166 )
167 })?;
168
169 Ok(value as i32)
170 }
171
172 pub fn load(&self) -> Result<(), Errno> {
185 self.install(FilterFlags::empty())?;
186 Ok(())
187 }
188
189 pub fn load_and_listen(&self) -> Result<Fd, Errno> {
193 let fd = self.install(FilterFlags::NEW_LISTENER)?;
194 Ok(Fd::new(fd))
195 }
196}
197
198impl Extend<sock_filter> for Filter {
199 fn extend<T: IntoIterator<Item = sock_filter>>(&mut self, iter: T) {
200 self.filter.extend(iter)
201 }
202}
203
204pub trait ByteCode {
206 fn into_bpf(self, filter: &mut Filter);
208}
209
210impl<F> ByteCode for F
211where
212 F: FnOnce(&mut Filter),
213{
214 fn into_bpf(self, filter: &mut Filter) {
215 self(filter)
216 }
217}
218
219impl ByteCode for sock_filter {
220 fn into_bpf(self, filter: &mut Filter) {
221 filter.push(self)
222 }
223}
224
225#[cfg(test)]
269macro_rules! seccomp_bpf {
270 ($($inst:expr),+ $(,)?) => {
271 {
272 let mut filter = Filter::new();
273 $(
274 $inst.into_bpf(&mut filter);
275 )+
276 filter
277 }
278 };
279}
280
281pub const fn BPF_STMT(code: u16, k: u32) -> sock_filter {
283 sock_filter {
284 code,
285 jt: 0,
286 jf: 0,
287 k,
288 }
289}
290
291pub const fn BPF_JUMP(code: u16, k: u32, jt: u8, jf: u8) -> sock_filter {
307 sock_filter { code, jt, jf, k }
308}
309
310pub const LOAD_SYSCALL_NR: sock_filter = BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_NR);
312
313#[allow(unused)]
315pub const ALLOW: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_ALLOW);
316
317#[allow(unused)]
320pub const DENY: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_THREAD);
321
322#[allow(unused)]
326pub const TRAP: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_TRAP);
327
328#[allow(unused)]
335pub fn TRACE(data: u16) -> sock_filter {
336 BPF_STMT(
337 BPF_RET + BPF_K,
338 libc::SECCOMP_RET_TRACE | (data as u32 & libc::SECCOMP_RET_DATA),
339 )
340}
341
342#[allow(unused)]
345pub fn ERRNO(err: Errno) -> sock_filter {
346 BPF_STMT(
347 BPF_RET + BPF_K,
348 libc::SECCOMP_RET_ERRNO | (err.into_raw() as u32 & libc::SECCOMP_RET_DATA),
349 )
350}
351
352macro_rules! instruction {
353 (
354 $(
355 $(#[$attrs:meta])*
356 $vis:vis fn $name:ident($($args:tt)*) {
357 $($instruction:expr;)*
358 }
359 )*
360 ) => {
361 $(
362 $vis fn $name($($args)*) -> impl ByteCode {
363 move |filter: &mut Filter| {
364 $(
365 $instruction.into_bpf(filter);
366 )*
367 }
368 }
369 )*
370 };
371}
372
373instruction! {
374 pub fn VALIDATE_ARCH(target_arch: u32) {
381 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_ARCH);
383 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, target_arch, 1, 0);
384 BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_PROCESS);
385 }
386
387 pub fn VALIDATE_ARCH_OR_ALTERNATE(target_arch: u32, alternate_arch: u32, action: sock_filter) {
392 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_ARCH);
394 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, target_arch, 3, 0);
396 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, alternate_arch, 0, 1);
398 action;
399 BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_PROCESS);
401 }
403
404 pub fn LOAD_SYSCALL_IP() {
405 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_IP_LO);
406 BPF_STMT(BPF_ST, 0);
408 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_IP_HI);
409 BPF_STMT(BPF_ST, 1);
411 }
412
413 pub fn SYSCALL(nr: Sysno, action: sock_filter) {
421 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr as i32 as u32, 0, 1);
422 action;
423 }
424
425 fn IP_RANGE64(blo: u32, bhi: u32, elo: u32, ehi: u32, action: sock_filter) {
426 BPF_JUMP(BPF_JMP + BPF_JGT + BPF_K, bhi, 4 , 0);
440 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, bhi, 0, 9 );
442 BPF_STMT(BPF_LD + BPF_MEM, 0);
444 BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, blo, 0, 7 );
446 BPF_STMT(BPF_LD + BPF_MEM, 1);
448
449 BPF_JUMP(BPF_JMP + BPF_JGT + BPF_K, ehi, 5 , 0);
453 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ehi, 0, 3 );
455 BPF_STMT(BPF_LD + BPF_MEM, 0);
457 BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, elo, 2 , 0);
459 BPF_STMT(BPF_LD + BPF_MEM, 1);
461
462 action;
464
465 BPF_STMT(BPF_LD + BPF_MEM, 1);
467 }
468}
469
470pub fn IP_EQ(ip: u64, action: sock_filter) -> impl ByteCode {
477 IP_EQ64(ip as u32, (ip >> 32) as u32, action)
478}
479
480instruction! {
481 fn IP_EQ64(lo: u32, hi: u32, action: sock_filter) {
482 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, hi, 0, 3 );
484 BPF_STMT(BPF_LD + BPF_MEM, 0);
486 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, lo, 0, 1 );
488 action;
490 BPF_STMT(BPF_LD + BPF_MEM, 1);
492 }
493}
494
495pub fn IP_RANGE(begin: u64, end: u64, action: sock_filter) -> impl ByteCode {
506 let begin_lo = begin as u32;
507 let begin_hi = (begin >> 32) as u32;
508 let end_lo = end as u32;
509 let end_hi = (end >> 32) as u32;
510
511 IP_RANGE64(begin_lo, begin_hi, end_lo, end_hi, action)
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn smoke() {
520 let filter = seccomp_bpf![
521 VALIDATE_ARCH(AUDIT_ARCH_X86_64),
522 LOAD_SYSCALL_NR,
523 SYSCALL(Sysno::openat, DENY),
524 SYSCALL(Sysno::close, DENY),
525 SYSCALL(Sysno::write, DENY),
526 SYSCALL(Sysno::read, DENY),
527 ALLOW,
528 ];
529
530 assert_eq!(filter.len(), 13);
531 }
532
533 const RET_ALLOW: u32 = libc::SECCOMP_RET_ALLOW;
534 const RET_MATCH: u32 = libc::SECCOMP_RET_TRACE | 1;
535
536 fn run(filter: &Filter, nr: u32, arch: u32, ip: u64) -> u32 {
541 const LD_ABS: u16 = BPF_LD + BPF_W + BPF_ABS;
542 const LD_MEM: u16 = BPF_LD + BPF_MEM;
543 const JEQ: u16 = BPF_JMP + BPF_JEQ + BPF_K;
544 const JGT: u16 = BPF_JMP + BPF_JGT + BPF_K;
545 const JGE: u16 = BPF_JMP + BPF_JGE + BPF_K;
546 const RET: u16 = BPF_RET + BPF_K;
547 let (mut acc, mut mem, mut pc) = (0u32, [0u32; 16], 0usize);
548 loop {
549 let insn = filter
550 .filter
551 .get(pc)
552 .unwrap_or_else(|| panic!("fell off the program at {pc}"));
553 pc += 1;
554 let jump = |taken: bool| usize::from(if taken { insn.jt } else { insn.jf });
555 match insn.code {
556 LD_ABS => {
557 acc = match insn.k {
558 SECCOMP_DATA_OFFSET_NR => nr,
559 SECCOMP_DATA_OFFSET_ARCH => arch,
560 SECCOMP_DATA_OFFSET_IP_LO => ip as u32,
561 SECCOMP_DATA_OFFSET_IP_HI => (ip >> 32) as u32,
562 k => panic!("load of unmodelled seccomp_data offset {k}"),
563 }
564 }
565 BPF_ST => mem[insn.k as usize] = acc,
566 LD_MEM => acc = mem[insn.k as usize],
567 JEQ => pc += jump(acc == insn.k),
568 JGT => pc += jump(acc > insn.k),
569 JGE => pc += jump(acc >= insn.k),
570 RET => return insn.k,
571 code => panic!("unmodelled opcode {code:#x} at {}", pc - 1),
572 }
573 }
574 }
575
576 fn range_filter(begin: u64, end: u64) -> Filter {
578 seccomp_bpf![
579 LOAD_SYSCALL_IP(),
580 IP_RANGE(begin, end, BPF_STMT(BPF_RET + BPF_K, RET_MATCH)),
581 ALLOW,
582 ]
583 }
584
585 fn matches(filter: &Filter, ip: u64) -> bool {
586 match run(filter, 0, AUDIT_ARCH_X86_64, ip) {
587 RET_MATCH => true,
588 RET_ALLOW => false,
589 other => panic!("unexpected verdict {other:#x} for ip {ip:#x}"),
590 }
591 }
592
593 fn probes(begin: u64, end: u64) -> Vec<u64> {
595 let mut ips = vec![
596 begin.wrapping_sub(1),
597 begin,
598 end.wrapping_sub(1),
599 end,
600 end.wrapping_add(1),
601 0x7fff_ffff,
602 0xffff_ffff,
603 0x1_0000_0000u64.wrapping_add(begin),
604 ];
605 for bound in [begin, end] {
607 ips.push(bound & !0xffff_ffff);
608 ips.push(bound | 0xffff_ffff);
609 }
610 ips
611 }
612
613 #[test]
617 fn ip_range_matches_exactly_the_half_open_interval() {
618 let ranges: &[(u64, u64)] = &[
619 (0x7100_0002, 0x7100_0003),
621 (0x1000, 0x2000),
622 (0x7fff_f000, 0x8000_1000),
623 (0xffff_f000, 0xffff_ffff),
624 (0x5_7100_0002, 0x5_7100_0003),
626 (0x7fff_0000_0000, 0x7fff_ffff_ffff),
627 (0xffff_f000, 0x1_0000_1000),
629 (0x7100_0002, 0x1_7100_0003),
630 (0x1_ffff_ffff, 0x2_0000_0001),
631 (0x1000, 0x7_0000_0000),
633 (0x3_8000_0000, 0x7fff_ffff_f000),
634 ];
635 let mut wrong = Vec::new();
636 for &(begin, end) in ranges {
637 let filter = range_filter(begin, end);
638 for ip in probes(begin, end) {
639 let expected = begin <= ip && ip < end;
640 if matches(&filter, ip) != expected {
641 wrong.push(format!(
642 "[{begin:#x}, {end:#x}) ip {ip:#x}: expected match={expected}"
643 ));
644 }
645 }
646 }
647 assert!(
648 wrong.is_empty(),
649 "{} wrong verdicts:\n{}",
650 wrong.len(),
651 wrong.join("\n")
652 );
653 }
654
655 #[test]
658 fn ip_range_matches_the_half_open_interval_over_a_bound_grid() {
659 let his = [0u64, 1, 2, 0x7fff, 0xffff_fffe, 0xffff_ffff];
660 let los = [
661 0u64,
662 1,
663 0x7100_0002,
664 0x7100_0003,
665 0x7fff_ffff,
666 0x8000_0000,
667 0xffff_fffe,
668 0xffff_ffff,
669 ];
670 let points: Vec<u64> = his
671 .iter()
672 .flat_map(|hi| los.iter().map(move |lo| (hi << 32) | lo))
673 .collect();
674 let (mut checked, mut wrong) = (0usize, Vec::new());
675 for &begin in &points {
676 for &end in points.iter().filter(|&&end| end > begin) {
677 let filter = range_filter(begin, end);
678 for &ip in points.iter().chain(&probes(begin, end)) {
679 checked += 1;
680 let expected = begin <= ip && ip < end;
681 if matches(&filter, ip) != expected {
682 wrong.push((begin, end, ip, expected));
683 }
684 }
685 }
686 }
687 assert!(checked > 60_000, "grid shrank to {checked} verdicts");
688 assert!(
689 wrong.is_empty(),
690 "{} of {checked} verdicts wrong; first: {:x?}",
691 wrong.len(),
692 &wrong[..wrong.len().min(8)]
693 );
694 }
695
696 #[test]
699 fn ip_ranges_chain_into_later_ranges_and_syscall_rules() {
700 use crate::seccomp::Action;
701 use crate::seccomp::FilterBuilder;
702
703 let filter = FilterBuilder::new()
704 .default_action(Action::Allow)
705 .target_arch(crate::seccomp::TargetArch::x86_64)
706 .ip_range(0x7100_0002, 0x7100_0003, Action::Trace(1))
707 .ip_range(0x1_0000_0000, 0x1_0000_1000, Action::Trace(2))
708 .syscall(Sysno::getppid, Action::Trace(3))
709 .build();
710 let nr = Sysno::getppid as u32;
711 let other = Sysno::getpid as u32;
712 let verdict = |nr, ip| run(&filter, nr, AUDIT_ARCH_X86_64, ip);
713 let trace = |data: u32| libc::SECCOMP_RET_TRACE | data;
714 for (nr, ip, expected) in [
715 (nr, 0x7100_0002, trace(1)),
716 (other, 0x7100_0002, trace(1)),
717 (nr, 0x7100_0003, trace(3)),
718 (nr, 0x7200_0002, trace(3)),
719 (other, 0x7200_0002, RET_ALLOW),
720 (nr, 0x1_0000_0000, trace(2)),
721 (nr, 0x1_0000_0fff, trace(2)),
722 (nr, 0x1_0000_1000, trace(3)),
723 (other, 0x1_0000_1000, RET_ALLOW),
724 (nr, 0x2_7100_0002, trace(3)),
725 ] {
726 assert_eq!(verdict(nr, ip), expected, "nr {nr} ip {ip:#x}");
727 }
728 assert_eq!(
729 run(&filter, nr, AUDIT_ARCH_X86_64 ^ 1, 0x7100_0002),
730 libc::SECCOMP_RET_KILL_PROCESS
731 );
732 }
733
734 #[cfg(target_arch = "x86_64")]
739 #[test]
740 fn kernel_ip_range_verdicts_match_the_interpreter() {
741 use crate::seccomp::Action;
742 use crate::seccomp::FilterBuilder;
743
744 const MATCHED: i32 = 10;
745 const UNMATCHED: i32 = 11;
746 const MAP_FAILED: i32 = 12;
747 const OTHER: i32 = 13;
748
749 fn kernel_matches(filter: &Filter, ip: u64) -> bool {
750 let page = 0x1000u64;
751 let first = (ip - 2) & !(page - 1);
752 let len = ((ip + 1 + page - 1) & !(page - 1)) - first;
753 match unsafe { libc::fork() } {
756 0 => unsafe {
757 let base = libc::mmap(
758 first as *mut libc::c_void,
759 len as usize,
760 libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
761 libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED_NOREPLACE,
762 -1,
763 0,
764 );
765 if base as u64 != first {
766 libc::_exit(MAP_FAILED);
767 }
768 let stub = [0x0f, 0x05, 0xc3u8];
770 std::ptr::copy_nonoverlapping(stub.as_ptr(), (ip - 2) as *mut u8, 3);
771 if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
772 || filter.load().is_err()
773 {
774 libc::_exit(OTHER);
775 }
776 let ret: i64;
777 std::arch::asm!(
778 "call {stub}",
779 stub = in(reg) ip - 2,
780 inlateout("rax") libc::SYS_getppid => ret,
781 out("rcx") _,
782 out("r11") _,
783 );
784 libc::_exit(if ret == -(libc::EXDEV as i64) {
785 MATCHED
786 } else if ret > 0 {
787 UNMATCHED
788 } else {
789 OTHER
790 });
791 },
792 -1 => panic!("fork failed: {}", std::io::Error::last_os_error()),
793 pid => {
794 let mut status = 0;
795 assert_eq!(unsafe { libc::waitpid(pid, &mut status, 0) }, pid);
796 assert!(libc::WIFEXITED(status), "child status {status:#x}");
797 match libc::WEXITSTATUS(status) {
798 MATCHED => true,
799 UNMATCHED => false,
800 MAP_FAILED => panic!("could not map a stub page at {first:#x}"),
801 code => panic!("child for ip {ip:#x} failed with {code}"),
802 }
803 }
804 }
805 }
806
807 let (mut unfaithful, mut wrong) = (Vec::new(), Vec::new());
808 for (begin, end, ips) in [
809 (
810 0x7100_0002u64,
811 0x7100_0003u64,
812 &[
813 0x7100_0001u64,
814 0x7100_0002,
815 0x7100_0003,
816 0x7200_0002,
817 0x7fff_ffff,
818 0xffff_ffff,
819 0x1_7100_0002,
820 ][..],
821 ),
822 (
823 0xffff_f000,
824 0x1_0000_1000,
825 &[0xffff_efff, 0xffff_f000, 0x1_0000_0fff, 0x1_0000_1000][..],
826 ),
827 ] {
828 let built = FilterBuilder::new()
829 .default_action(Action::Allow)
830 .ip_range(begin, end, Action::Errno(Errno::EXDEV))
831 .build();
832 for &ip in ips {
833 let expected = begin <= ip && ip < end;
834 let interpreted = run(&built, Sysno::getppid as u32, AUDIT_ARCH_X86_64, ip)
835 == (libc::SECCOMP_RET_ERRNO | libc::EXDEV as u32);
836 let kernel = kernel_matches(&built, ip);
837 let row = format!("[{begin:#x}, {end:#x}) ip {ip:#x}: kernel match={kernel}");
838 if kernel != interpreted {
839 unfaithful.push(format!("{row}, interpreter match={interpreted}"));
840 }
841 if kernel != expected {
842 wrong.push(format!("{row}, interval match={expected}"));
843 }
844 }
845 }
846 assert!(
847 unfaithful.is_empty(),
848 "interpreter disagrees with the kernel:\n{}",
849 unfaithful.join("\n")
850 );
851 assert!(
852 wrong.is_empty(),
853 "kernel verdicts outside the interval:\n{}",
854 wrong.join("\n")
855 );
856 }
857}