Mutation testing pauses until P12 (D-33, owner 2026-10-20), which reruns
the whole max-testing pass over everything since P1. Before the pause,
pass 1 on slices 3-6 and its triage gave tests in ledger-store,
ledger-package and ledger-mcp, and three real bugs, fixed:
- the command line took an admin answer longer than four MiB, or cut off
before its newline, whenever the bytes that came still parsed;
- The CPA package put a twin's number on the last dot of the whole path,
so in a folder with a dot the second file landed in another folder;
- a document or category named of dots alone ("..") could climb out of
its folder when the CPA package is unpacked.
The plan gains P13, bookspec (D-34): a formal, sourced accounting
specification first (rules with symbolic forms and worked examples), then
its own formal validation and a conformance harness any engine can be
tested against, seeded by P3's regression tests. docs/testing.md records
slices 3-8 and what P12 must cover; plan/STATUS.md says where P2 stands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
94 lines
3.3 KiB
Rust
94 lines
3.3 KiB
Rust
//! Tests only: system calls refused, as a disk or a file system can
|
|
//! refuse them, in one thread of the test's own. A seccomp filter makes
|
|
//! each named call fail at once with the error chosen, before the kernel
|
|
//! looks at any path, so a refusal no real folder can be made to give
|
|
//! (a lookup that fails where making the folder would not; a link
|
|
//! refused where the name is already taken) is given every time. The
|
|
//! filter holds the calling thread only and ends with it: the rest of
|
|
//! the test process is untouched.
|
|
#![cfg(target_os = "linux")]
|
|
|
|
/// Run `f` in a thread of its own in which each call in `calls` fails
|
|
/// with `errno`, and give back what it returns; None where the system
|
|
/// won't put the filter on (the test then has nothing to show).
|
|
pub(crate) fn refusing<T: Send>(
|
|
calls: &[libc::c_long],
|
|
errno: i32,
|
|
f: impl FnOnce() -> T + Send,
|
|
) -> Option<T> {
|
|
std::thread::scope(|s| {
|
|
s.spawn(|| {
|
|
// Looked up once before the filter, so std knows statx is
|
|
// there and never mistakes a refused one for a missing one.
|
|
let _ = std::fs::symlink_metadata("/");
|
|
filter(calls, errno).then(f)
|
|
})
|
|
.join()
|
|
.unwrap()
|
|
})
|
|
}
|
|
|
|
/// Put the filter on this thread: the call's number checked against
|
|
/// each of `calls`, a match refused with `errno`, anything else let be.
|
|
fn filter(calls: &[libc::c_long], errno: i32) -> bool {
|
|
use libc::{
|
|
BPF_ABS, BPF_JEQ, BPF_JMP, BPF_K, BPF_LD, BPF_RET, BPF_W, SECCOMP_RET_ALLOW,
|
|
SECCOMP_RET_ERRNO, sock_filter,
|
|
};
|
|
let stmt = |code: u32, k: u32| sock_filter {
|
|
code: code as u16,
|
|
jt: 0,
|
|
jf: 0,
|
|
k,
|
|
};
|
|
let n = calls.len();
|
|
let mut prog = vec![stmt(
|
|
BPF_LD | BPF_W | BPF_ABS,
|
|
std::mem::offset_of!(libc::seccomp_data, nr) as u32,
|
|
)];
|
|
for (i, call) in calls.iter().enumerate() {
|
|
prog.push(sock_filter {
|
|
code: (BPF_JMP | BPF_JEQ | BPF_K) as u16,
|
|
// To the refusal, past the other checks and the allow.
|
|
jt: u8::try_from(n - i).unwrap(),
|
|
jf: 0,
|
|
k: u32::try_from(*call).unwrap(),
|
|
});
|
|
}
|
|
prog.push(stmt(BPF_RET | BPF_K, SECCOMP_RET_ALLOW));
|
|
prog.push(stmt(
|
|
BPF_RET | BPF_K,
|
|
SECCOMP_RET_ERRNO | u32::try_from(errno).unwrap(),
|
|
));
|
|
let fprog = libc::sock_fprog {
|
|
len: u16::try_from(prog.len()).unwrap(),
|
|
filter: prog.as_mut_ptr(),
|
|
};
|
|
// SAFETY: prctl with a filter program that outlives the call (the
|
|
// kernel copies it); without TSYNC it binds this thread alone.
|
|
unsafe {
|
|
libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0
|
|
&& libc::prctl(
|
|
libc::PR_SET_SECCOMP,
|
|
libc::SECCOMP_MODE_FILTER,
|
|
&fprog as *const libc::sock_fprog,
|
|
) == 0
|
|
}
|
|
}
|
|
|
|
/// The calls that look a name up without following it (std's
|
|
/// `symlink_metadata`): statx, and what std and libc fall back to.
|
|
pub(crate) const LOOKUPS: &[libc::c_long] = &[
|
|
libc::SYS_statx,
|
|
libc::SYS_newfstatat,
|
|
#[cfg(target_arch = "x86_64")]
|
|
libc::SYS_lstat,
|
|
];
|
|
|
|
/// The calls that make a hard link (std's `hard_link`).
|
|
pub(crate) const LINKS: &[libc::c_long] = &[
|
|
libc::SYS_linkat,
|
|
#[cfg(target_arch = "x86_64")]
|
|
libc::SYS_link,
|
|
];
|