RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

foundry / libctest.rs
31543 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
//! libctest -- stage 16, "libc batch 1": the system calls a C library
//! (musl) and its shell (dash) need beyond open/read/write. Run with one
//! group name; exit code = number of failed checks:
//!   wait     dash's `wait`: block SIGCHLD, fork, rt_sigsuspend until the
//!            handler ran (before 16 rt_sigsuspend was ENOSYS: dash spun
//!            forever)
//!   signals  rt_sigpending, rt_sigtimedwait, rt_sigqueueinfo (real-time
//!            signals queue up), sigaltstack + SA_ONSTACK, unknown sa_flags
//!   locks    fcntl: F_GETFL/F_SETFL, F_DUPFD limits, record locks between
//!            two processes (F_GETLK/F_SETLK/F_SETLKW, dropped on close/exit)
//!   files    truncate/ftruncate, fsync, chmod family, umask, utimensat and
//!            real stat times, on ramfs, ext2, FAT and (read-only) btrfs
//!   misc     rlimits, prctl, sched_getaffinity, madvise, robust futex lists
//!   orphans  children whose parent exits without waiting (the kernel test
//!            checks their memory comes back), and PR_SET_CHILD_SUBREAPER
#![no_std]
#![no_main]

#[macro_use]
#[path = "ulib.rs"]
mod ulib;

extern crate alloc;

use core::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use ulib::*;

// Stage 16 syscall numbers (Linux's).
const SYS_MADVISE: u64 = 28;
const SYS_FSYNC: u64 = 74;
const SYS_FDATASYNC: u64 = 75;
const SYS_TRUNCATE: u64 = 76;
const SYS_FTRUNCATE: u64 = 77;
const SYS_CHMOD: u64 = 90;
const SYS_FCHMOD: u64 = 91;
const SYS_UMASK: u64 = 95;
const SYS_GETRLIMIT: u64 = 97;
const SYS_RT_SIGPENDING: u64 = 127;
const SYS_RT_SIGTIMEDWAIT: u64 = 128;
const SYS_RT_SIGQUEUEINFO: u64 = 129;
const SYS_RT_SIGSUSPEND: u64 = 130;
const SYS_SIGALTSTACK: u64 = 131;
const SYS_PRCTL: u64 = 157;
const SYS_SETRLIMIT: u64 = 160;
const SYS_GETTID: u64 = 186;
const SYS_SCHED_GETAFFINITY: u64 = 204;
const SYS_FCHMODAT: u64 = 268;
const SYS_SET_ROBUST_LIST: u64 = 273;
const SYS_GET_ROBUST_LIST: u64 = 274;
const SYS_UTIMENSAT: u64 = 280;
const SYS_PRLIMIT64: u64 = 302;

const O_RDWR: u64 = 2;
const O_NONBLOCK: u64 = 0o4000;
const F_DUPFD: u64 = 0;
const F_GETFL: u64 = 3;
const F_SETFL: u64 = 4;
const F_GETLK: u64 = 5;
const F_SETLK: u64 = 6;
const F_SETLKW: u64 = 7;
const F_RDLCK: i16 = 0;
const F_WRLCK: i16 = 1;
const F_UNLCK: i16 = 2;
const EROFS: i64 = 30;
const EMFILE: i64 = 24;
const ENOMEM: i64 = 12;
const EDEADLK: i64 = 35;
const EOPNOTSUPP: i64 = 95;
const SA_ONSTACK: u64 = 0x0800_0000;
const SS_ONSTACK: u32 = 1;
const SS_DISABLE: u32 = 2;
const SI_QUEUE: i32 = -1;
const RLIMIT_STACK: u64 = 3;
const RLIMIT_NOFILE: u64 = 7;
const UTIME_NOW: i64 = (1 << 30) - 1;
const UTIME_OMIT: i64 = (1 << 30) - 2;
const FUTEX_OWNER_DIED: u32 = 0x4000_0000;

fn sys4(nr: u64, a: u64, b: u64, c: u64, d: u64) -> i64 {
    syscall5(nr, a, b, c, d, 0)
}

/// `struct flock`.
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct Flock {
    l_type: i16,
    l_whence: i16,
    l_start: i64,
    l_len: i64,
    l_pid: i32,
    _pad: i32,
}

fn lock(fd: i64, cmd: u64, l_type: i16, start: i64, len: i64) -> (i64, Flock) {
    let mut fl = Flock { l_type, l_whence: SEEK_SET as i16, l_start: start, l_len: len, ..Default::default() };
    let r = syscall3(SYS_FCNTL, fd as u64, cmd, &mut fl as *mut Flock as u64);
    (r, fl)
}

fn cstr(s: &str) -> alloc::vec::Vec<u8> {
    let mut v = alloc::vec::Vec::from(s.as_bytes());
    v.push(0);
    v
}

fn path_call(nr: u64, path: &str, b: u64) -> i64 {
    with_cpath(path, |p| syscall3(nr, p, b, 0))
}

fn file_size(path: &str) -> i64 {
    let mut st = Stat::default();
    if stat(path, &mut st) < 0 { -1 } else { st.size }
}

fn perm(path: &str) -> u32 {
    let mut st = Stat::default();
    stat(path, &mut st);
    st.perm()
}

fn write_file(path: &str, data: &[u8]) -> i64 {
    let fd = open(path, O_WRONLY | O_CREAT | O_TRUNC);
    if fd < 0 {
        return fd;
    }
    let n = write(fd, data);
    close(fd);
    n
}

fn umask(mask: u64) -> u64 {
    syscall3(SYS_UMASK, mask, 0, 0) as u64
}

static GOT: AtomicI64 = AtomicI64::new(0);
static SP_SEEN: AtomicU64 = AtomicU64::new(0);
static ONSTACK_FLAGS: AtomicU64 = AtomicU64::new(0);
static CHANGE_ON_STACK: AtomicI64 = AtomicI64::new(0);

extern "C" fn count_handler(_sig: i32) {
    GOT.fetch_add(1, Ordering::SeqCst);
}

/// SA_ONSTACK handler: where is my stack, does sigaltstack say SS_ONSTACK,
/// and may I change the alternate stack while on it (no: EPERM)?
extern "C" fn altstack_handler(_sig: i32) {
    let local = 0u8;
    SP_SEEN.store(&local as *const u8 as u64, Ordering::SeqCst);
    let mut old = [0u64; 3];
    syscall3(SYS_SIGALTSTACK, 0, old.as_mut_ptr() as u64, 0);
    ONSTACK_FLAGS.store(old[1], Ordering::SeqCst);
    let new = [0u64, SS_DISABLE as u64, 0];
    CHANGE_ON_STACK.store(syscall3(SYS_SIGALTSTACK, new.as_ptr() as u64, 0, 0), Ordering::SeqCst);
}

fn main() -> i64 {
    let group = arg(1).unwrap_or("");
    let mut c = Checks::new("libctest");
    match group {
        "wait" => wait_group(&mut c),
        "signals" => signals(&mut c),
        "locks" => locks(&mut c),
        "files" => files(&mut c),
        "misc" => misc(&mut c),
        "orphans" => orphans(&mut c),
        _ => c.check(false, "usage: libctest wait|signals|locks|files|misc|orphans"),
    }
    c.done()
}

// --- wait: what dash does for `wait` / a foreground job ----------------------

fn wait_group(c: &mut Checks) {
    GOT.store(0, Ordering::SeqCst);
    signal(SIGCHLD, count_handler);
    let old = sigprocmask(SIG_BLOCK, sigbit(SIGCHLD)) as u64;
    let pid = fork();
    if pid == 0 {
        sleep_ms(60);
        exit(7);
    }
    // dash: while no child changed state, sigsuspend(old mask) -- each
    // return is a handled signal (EINTR). Bounded so a broken kernel fails
    // the check instead of hanging the test like dash did.
    let empty = old & !sigbit(SIGCHLD);
    let mut rounds = 0;
    let mut all_eintr = true;
    while GOT.load(Ordering::SeqCst) == 0 && rounds < 20 {
        let r = syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 8, 0);
        all_eintr &= r == -EINTR;
        rounds += 1;
    }
    c.check(all_eintr, "rt_sigsuspend returns EINTR after a handler ran");
    c.check(GOT.load(Ordering::SeqCst) == 1 && rounds == 1, "one sigsuspend: woken by the child's SIGCHLD");
    let mask_now = sigprocmask(SIG_BLOCK, 0) as u64;
    c.check(mask_now & sigbit(SIGCHLD) != 0, "the old mask (SIGCHLD blocked) is back afterwards");
    let st = wait4(pid, 0);
    c.check(matches!(st, Ok((p, s)) if p == pid && wexitstatus(s) == 7), "wait4 then reaps the child (status 7)");
    c.check(syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 4, 0) == -EINVAL, "rt_sigsuspend with size 4: EINVAL");
    // SIGCHLD that arrived while blocked: sigsuspend returns at once.
    let pid = fork();
    if pid == 0 {
        exit(0);
    }
    while try_wait(pid) == -EAGAIN && GOT.load(Ordering::SeqCst) == 1 {
        sleep_ms(5); // until the child is a zombie (SIGCHLD pending)
    }
    sleep_ms(20);
    let r = syscall3(SYS_RT_SIGSUSPEND, &empty as *const u64 as u64, 8, 0);
    c.check(r == -EINTR && GOT.load(Ordering::SeqCst) == 2, "a SIGCHLD already pending ends sigsuspend right away");
    wait4(-1, 0).ok();
    sigprocmask(SIG_SETMASK, old);
}

// --- signals -----------------------------------------------------------------

fn timedwait(set: u64, info: &mut [u64; 16], timeout: Option<[i64; 2]>) -> i64 {
    let ts = timeout.unwrap_or([0, 0]);
    let tsp = if timeout.is_some() { &ts as *const [i64; 2] as u64 } else { 0 };
    sys4(SYS_RT_SIGTIMEDWAIT, &set as *const u64 as u64, info.as_mut_ptr() as u64, tsp, 8)
}

fn sigqueue(pid: i64, sig: i32, code: i32, value: u64) -> i64 {
    let mut info = [0u64; 16];
    info[0] = sig as u32 as u64; // si_signo (si_errno = 0)
    info[1] = code as u32 as u64; // si_code (low half of the 2nd word)
    info[2] = getpid() as u64; // si_pid, si_uid
    info[3] = value; // si_value
    syscall3(SYS_RT_SIGQUEUEINFO, pid as u64, sig as u64, info.as_ptr() as u64)
}

fn signals(c: &mut Checks) {
    let mut info = [0u64; 16];
    // rt_sigpending
    signal(SIGUSR1, count_handler);
    GOT.store(0, Ordering::SeqCst);
    let old = sigprocmask(SIG_BLOCK, sigbit(SIGUSR1) | sigbit(SIGUSR2)) as u64;
    kill(getpid(), SIGUSR1);
    let mut pending = 0u64;
    c.check(syscall3(SYS_RT_SIGPENDING, &mut pending as *mut u64 as u64, 8, 0) == 0, "rt_sigpending: 0");
    c.check(pending & sigbit(SIGUSR1) != 0 && GOT.load(Ordering::SeqCst) == 0, "a blocked SIGUSR1 shows as pending, not delivered");
    c.check(syscall3(SYS_RT_SIGPENDING, &mut pending as *mut u64 as u64, 16, 0) == -EINVAL, "rt_sigpending with size 16: EINVAL");
    sigprocmask(SIG_UNBLOCK, sigbit(SIGUSR1));
    c.check(GOT.load(Ordering::SeqCst) == 1, "unblocking delivers it");

    // rt_sigtimedwait: takes a pending signal without running a handler.
    kill(getpid(), SIGUSR2);
    let r = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));
    c.check(r == SIGUSR2 as i64 && info[0] as u32 == SIGUSR2 as u32, "sigtimedwait takes a pending SIGUSR2 (and fills siginfo)");
    c.check(timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0])) == -EAGAIN, "nothing pending, timeout {0,0}: EAGAIN at once");
    let t0 = now_us();
    let r = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 120_000_000]));
    c.check(r == -EAGAIN && now_us() - t0 >= 100_000, "nothing comes within 120 ms: EAGAIN after the timeout");
    let bad = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 1_000_000_000]));
    c.check(bad == -EINVAL, "tv_nsec of 1e9: EINVAL");
    let me = getpid();
    let child = fork();
    if child == 0 {
        sleep_ms(50);
        kill(me, SIGUSR2);
        exit(0);
    }
    let r = timedwait(sigbit(SIGUSR2), &mut info, None);
    c.check(r == SIGUSR2 as i64 && (info[2] & 0xffff_ffff) as i64 == child, "sigtimedwait sleeps until another process sends it (si_pid = sender)");
    wait(child);

    // rt_sigqueueinfo: real-time signals queue, each with its own value.
    let rt = 40;
    sigprocmask(SIG_BLOCK, sigbit(rt));
    for v in [11u64, 22, 33] {
        sigqueue(me, rt, SI_QUEUE, v);
    }
    let mut values = alloc::vec::Vec::new();
    while timedwait(sigbit(rt), &mut info, Some([0, 0])) == rt as i64 {
        values.push(info[3]);
        c.check(info[1] as u32 as i32 == SI_QUEUE, "queued signal keeps si_code SI_QUEUE");
    }
    c.check(values == [11, 22, 33], "a real-time signal sent 3 times arrives 3 times, in order, with its values");
    sigqueue(me, SIGUSR2, SI_QUEUE, 1);
    sigqueue(me, SIGUSR2, SI_QUEUE, 2);
    let first = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));
    let second = timedwait(sigbit(SIGUSR2), &mut info, Some([0, 0]));
    c.check(first == SIGUSR2 as i64 && second == -EAGAIN, "a standard signal sent twice is pending once");
    let child = fork();
    if child == 0 {
        sleep_ms(200);
        exit(0);
    }
    c.check(sigqueue(child, SIGUSR2, 0, 0) == -EPERM, "si_code >= 0 (pretending to be kill) to another process: EPERM");
    c.check(sigqueue(child, 0, SI_QUEUE, 0) == 0, "signal 0 to a live process: 0");
    kill(child, SIGKILL);
    wait(child);
    c.check(sigqueue(99_999, SIGUSR2, SI_QUEUE, 0) == -ESRCH, "to a pid that doesn't exist: ESRCH");

    // sigaltstack
    let mut old_ss = [0u64; 3];
    syscall3(SYS_SIGALTSTACK, 0, old_ss.as_mut_ptr() as u64, 0);
    c.check(old_ss[1] as u32 == SS_DISABLE, "no alternate stack at first (SS_DISABLE)");
    let size = 64 * 1024u64;
    let stack = mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;
    let small = [stack, 0, 1024];
    c.check(syscall3(SYS_SIGALTSTACK, small.as_ptr() as u64, 0, 0) == -ENOMEM, "a 1 KiB alternate stack: ENOMEM");
    let badflags = [stack, 0x10, size];
    c.check(syscall3(SYS_SIGALTSTACK, badflags.as_ptr() as u64, 0, 0) == -EINVAL, "unknown ss_flags: EINVAL");
    let ss = [stack, 0, size];
    c.check(syscall3(SYS_SIGALTSTACK, ss.as_ptr() as u64, 0, 0) == 0, "set a 64 KiB alternate stack");
    sigaction_raw(SIGUSR1, altstack_handler as usize, SA_ONSTACK, 0);
    kill(me, SIGUSR1);
    let sp = SP_SEEN.load(Ordering::SeqCst);
    c.check((stack..stack + size).contains(&sp), "an SA_ONSTACK handler runs on the alternate stack");
    c.check(ONSTACK_FLAGS.load(Ordering::SeqCst) as u32 == SS_ONSTACK, "inside it, sigaltstack reports SS_ONSTACK");
    c.check(CHANGE_ON_STACK.load(Ordering::SeqCst) == -EPERM, "changing the alternate stack while on it: EPERM");
    syscall3(SYS_SIGALTSTACK, 0, old_ss.as_mut_ptr() as u64, 0);
    c.check(old_ss == [stack, 0, size], "afterwards it's still set, flags 0 (not on it)");
    let off = [0u64, SS_DISABLE as u64, 0];
    syscall3(SYS_SIGALTSTACK, off.as_ptr() as u64, 0, 0);
    munmap(stack, size);

    // Unknown sa_flags bits aren't stored (musl checks what it got back).
    let act = SigAction { handler: count_handler as usize, flags: 0x100 | 0x0400_0000, restorer: 0, mask: 0 };
    let mut back = SigAction::default();
    syscall5(SYS_RT_SIGACTION, SIGUSR2 as u64, &act as *const _ as u64, 0, 8, 0);
    syscall5(SYS_RT_SIGACTION, SIGUSR2 as u64, 0, &mut back as *mut _ as u64, 8, 0);
    c.check(back.flags & 0x100 == 0 && back.flags & 0x0400_0000 != 0, "unknown sa_flags bits are dropped, known ones kept");
    sigaction_raw(SIGUSR1, SIG_DFL, 0, 0);
    sigaction_raw(SIGUSR2, SIG_DFL, 0, 0);
    sigprocmask(SIG_SETMASK, old);
}

// --- locks -------------------------------------------------------------------

fn locks(c: &mut Checks) {
    let path = "/tmp/libctest-locks";
    write_file(path, b"0123456789abcdefghijklmnopqrstuvwxyz");
    // F_GETFL tells the access mode and O_APPEND; F_SETFL changes O_APPEND.
    let ro = open(path, O_RDONLY);
    let wa = open(path, O_WRONLY | O_APPEND);
    c.check(fcntl(ro, F_GETFL, 0) & 3 == 0, "F_GETFL of an O_RDONLY fd: O_RDONLY");
    c.check(fcntl(wa, F_GETFL, 0) as u64 & (3 | O_APPEND) == O_WRONLY | O_APPEND, "F_GETFL: O_WRONLY | O_APPEND");
    fcntl(wa, F_SETFL, O_NONBLOCK);
    c.check(fcntl(wa, F_GETFL, 0) as u64 & (O_APPEND | O_NONBLOCK) == O_NONBLOCK, "F_SETFL clears O_APPEND, sets O_NONBLOCK");
    c.check(lock(ro, F_SETLK, F_WRLCK, 0, 1).0 == -EBADF, "a write lock through a read-only fd: EBADF");
    c.check(lock(wa, F_SETLK, F_RDLCK, 0, 1).0 == -EBADF, "a read lock through a write-only fd: EBADF");
    c.check(lock(ro, F_SETLK, 7, 0, 1).0 == -EINVAL, "l_type 7: EINVAL");
    c.check(lock(ro, F_SETLK, F_RDLCK, -5, 1).0 == -EINVAL, "a range starting before 0: EINVAL");
    close(ro);
    close(wa);

    // Two processes.
    let fd = open(path, O_RDWR);
    c.check(lock(fd, F_SETLK, F_WRLCK, 0, 10).0 == 0, "parent: write lock on bytes 0..10");
    let (r, fl) = lock(fd, F_GETLK, F_WRLCK, 0, 10);
    c.check(r == 0 && fl.l_type == F_UNLCK, "F_GETLK: my own locks never conflict");
    let parent = getpid();
    let (rd, wr) = pipe().unwrap();
    let child = fork();
    if child == 0 {
        let mut k = Checks::new("libctest-child");
        let fd = open(path, O_RDWR);
        let (r, fl) = lock(fd, F_GETLK, F_RDLCK, 5, 1);
        k.check(r == 0 && fl.l_type == F_WRLCK && fl.l_pid as i64 == parent && fl.l_start == 0 && fl.l_len == 10, "child: F_GETLK names the parent's lock");
        k.check(lock(fd, F_SETLK, F_RDLCK, 9, 5).0 == -EAGAIN, "child: overlapping F_SETLK: EAGAIN");
        k.check(lock(fd, F_SETLK, F_WRLCK, 10, 10).0 == 0, "child: bytes 10..20 are free");
        write(wr, b"x"); // tell the parent we're about to wait
        let t0 = now_us();
        let r = lock(fd, F_SETLKW, F_WRLCK, 0, 10).0;
        k.check(r == 0 && now_us() - t0 >= 50_000, "child: F_SETLKW waits until the parent's lock goes away");
        // The parent now asks for 10..20 (ours) with F_SETLKW and sleeps;
        // us asking for its 30..40 would close the circle: EDEADLK.
        sleep_ms(100);
        k.check(lock(fd, F_SETLKW, F_WRLCK, 30, 5).0 == -EDEADLK, "child: a lock wait that would deadlock: EDEADLK");
        exit(k.done()); // exiting drops all our locks
    }
    let mut b = [0u8; 1];
    read(rd, &mut b);
    sleep_ms(100);
    c.check(lock(fd, F_SETLK, F_WRLCK, 30, 10).0 == 0, "parent: write lock on 30..40");
    let fd2 = open(path, O_RDONLY);
    close(fd2); // closing *any* fd of the file drops my locks on it
    // (the child now gets 0..10; we wait for its 10..20, which it holds
    // until it exits)
    c.check(lock(fd, F_SETLK, F_WRLCK, 30, 10).0 == 0, "parent: 30..40 again (the close dropped it)");
    let r = lock(fd, F_SETLKW, F_WRLCK, 10, 10).0;
    c.check(r == 0, "parent: F_SETLKW on the child's range succeeds once it exits");
    c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "child's checks passed");
    c.check(lock(fd, F_SETLK, F_WRLCK, 0, 0).0 == 0, "the whole file is free after the child exited");
    c.check(lock(fd, F_SETLK, F_UNLCK, 0, 0).0 == 0, "F_UNLCK");
    close(fd);
    close(rd);
    close(wr);
    unlink(path);

    // RLIMIT_NOFILE is the fd table's real size; F_DUPFD honours it.
    let mut lim = [0u64; 2];
    syscall3(SYS_GETRLIMIT, RLIMIT_NOFILE, lim.as_mut_ptr() as u64, 0);
    c.check(lim == [64, 64], "RLIMIT_NOFILE is 64 (the fd table size)");
    c.check(fcntl(0, F_DUPFD, 64) == -EINVAL, "F_DUPFD at 64: EINVAL");
    let small = [8u64, 64];
    syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, small.as_ptr() as u64, 0);
    c.check(fcntl(0, F_DUPFD, 8) == -EINVAL, "after setrlimit(NOFILE, 8): F_DUPFD at 8 is EINVAL");
    let mut fds = alloc::vec::Vec::new();
    loop {
        let fd = dup(0);
        if fd < 0 {
            c.check(fd == -EMFILE && fds.iter().all(|&f| f < 8), "dup fails with EMFILE at the soft limit");
            break;
        }
        fds.push(fd);
    }
    for fd in fds {
        close(fd);
    }
    let back = [64u64, 64];
    c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, back.as_ptr() as u64, 0) == 0, "raising it back up to the hard limit");
}

// --- files -------------------------------------------------------------------

fn files(c: &mut Checks) {
    // truncate / ftruncate on ramfs, ext2, FAT.
    for dir in ["/tmp", "/mnt/disk1", "/mnt/disk0"] {
        let path = alloc::format!("{}/trunc16.txt", dir);
        write_file(&path, b"hello world");
        let fd = open(&path, O_RDWR);
        c.check(syscall3(SYS_FTRUNCATE, fd as u64, 5, 0) == 0 && file_size(&path) == 5, &alloc::format!("{}: ftruncate to 5 bytes", dir));
        c.check(syscall3(SYS_FTRUNCATE, fd as u64, 9000, 0) == 0 && file_size(&path) == 9000, &alloc::format!("{}: ftruncate grows to 9000", dir));
        let mut buf = [0xffu8; 16];
        let n = syscall6(SYS_PREAD64_NR, fd as u64, buf.as_mut_ptr() as u64, 16, 0, 0, 0);
        c.check(n == 16 && &buf[..5] == b"hello" && buf[5..].iter().all(|&b| b == 0), &alloc::format!("{}: the old bytes stay, the new ones are zeros", dir));
        c.check(syscall3(SYS_FSYNC, fd as u64, 0, 0) == 0 && syscall3(SYS_FDATASYNC, fd as u64, 0, 0) == 0, &alloc::format!("{}: fsync, fdatasync: 0", dir));
        close(fd);
        c.check(path_call(SYS_TRUNCATE, &path, 3) == 0 && file_size(&path) == 3, &alloc::format!("{}: truncate(path, 3)", dir));
        let ro = open(&path, O_RDONLY);
        c.check(syscall3(SYS_FTRUNCATE, ro as u64, 0, 0) == -EINVAL, &alloc::format!("{}: ftruncate of a read-only fd: EINVAL", dir));
        c.check(syscall3(SYS_FTRUNCATE, ro as u64, -1i64 as u64, 0) == -EINVAL, &alloc::format!("{}: negative length: EINVAL", dir));
        close(ro);
        unlink(&path);
    }
    c.check(path_call(SYS_TRUNCATE, "/tmp", 0) == -EISDIR, "truncate of a directory: EISDIR");
    c.check(path_call(SYS_TRUNCATE, "/mnt/disk3/hello.txt", 0) == -EROFS, "truncate on btrfs (read-only here): EROFS");
    c.check(path_call(SYS_TRUNCATE, "/tmp/nope16", 0) == -ENOENT, "truncate of a missing file: ENOENT");
    let (rd, wr) = pipe().unwrap();
    c.check(syscall3(SYS_FSYNC, rd as u64, 0, 0) == -EINVAL, "fsync of a pipe: EINVAL");
    c.check(syscall3(SYS_FTRUNCATE, wr as u64, 0, 0) == -EINVAL, "ftruncate of a pipe: EINVAL");
    close(rd);
    close(wr);

    // umask: taken away from open(O_CREAT) and mkdir modes; per process.
    let old = umask(0o077);
    c.check(old == 0o022, "umask starts as 022 and returns the old value");
    write_file("/tmp/mask16", b"x");
    c.check(perm("/tmp/mask16") == 0o600, "with umask 077, open(O_CREAT, 0644) makes 0600");
    with_cpath("/tmp/maskdir16", |p| syscall3(SYS_MKDIR, p, 0o777, 0));
    c.check(perm("/tmp/maskdir16") == 0o700, "and mkdir(0777) makes 0700");
    let child = fork();
    if child == 0 {
        exit(if umask(0) == 0o077 { 0 } else { 1 });
    }
    c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "fork inherits the umask");
    c.check(umask(old) == 0o077, "umask(022) gives back 077");
    unlink("/tmp/mask16");
    with_cpath("/tmp/maskdir16", |p| syscall3(SYS_RMDIR, p, 0, 0));

    // chmod family.
    for dir in ["/tmp", "/mnt/disk1"] {
        let path = alloc::format!("{}/mode16", dir);
        write_file(&path, b"m");
        c.check(path_call(SYS_CHMOD, &path, 0o751) == 0 && perm(&path) == 0o751, &alloc::format!("{}: chmod 751", dir));
        let fd = open(&path, O_RDONLY);
        c.check(syscall3(SYS_FCHMOD, fd as u64, 0, 0) == 0 && perm(&path) == 0, &alloc::format!("{}: fchmod 000 (a real 0, not 'unknown')", dir));
        close(fd);
        let name = cstr("mode16");
        let dfd = open(dir, O_RDONLY | O_DIRECTORY);
        c.check(sys4(SYS_FCHMODAT, dfd as u64, name.as_ptr() as u64, 0o4644, 0) == 0 && perm(&path) == 0o4644, &alloc::format!("{}: fchmodat relative to a dir fd, setuid bit kept", dir));
        close(dfd);
        unlink(&path);
    }
    c.check(path_call(SYS_CHMOD, "/mnt/disk0/readme.txt", 0o600) == -EPERM, "chmod on FAT (no permissions to keep): EPERM");
    c.check(path_call(SYS_CHMOD, "/mnt/disk3/hello.txt", 0o600) == -EROFS, "chmod on btrfs: EROFS");
    // (Rustbelt can't make symlinks yet; btrfs has some.)
    let link = cstr("/mnt/disk3/hello-link");
    let r = sys4(452, AT_FDCWD as u64, link.as_ptr() as u64, 0o700, AT_SYMLINK_NOFOLLOW);
    c.check(r == -EOPNOTSUPP, "fchmodat2(AT_SYMLINK_NOFOLLOW) on a symlink: EOPNOTSUPP (like Linux)");

    // utimensat + stat times.
    for (dir, nsec_kept) in [("/tmp", true), ("/mnt/disk1", false)] {
        let path = alloc::format!("{}/times16", dir);
        write_file(&path, b"t");
        let mut st = Stat::default();
        stat(&path, &mut st);
        c.check(st.mtime[0] > 1_000_000_000, &alloc::format!("{}: a new file's mtime is now (not 1970)", dir));
        let p = cstr(&path);
        let times = [[1000i64, 5], [2000, 6]];
        let r = sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, times.as_ptr() as u64, 0);
        stat(&path, &mut st);
        let want = if nsec_kept { ([1000, 5], [2000, 6]) } else { ([1000, 0], [2000, 0]) };
        c.check(r == 0 && st.atime == want.0 && st.mtime == want.1, &alloc::format!("{}: utimensat sets atime and mtime", dir));
        c.check(st.ctime[0] > 1_000_000_000, &alloc::format!("{}: ... and ctime becomes now", dir));
        let omit = [[0i64, UTIME_OMIT], [3000, 0]];
        sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, omit.as_ptr() as u64, 0);
        stat(&path, &mut st);
        c.check(st.atime[0] == 1000 && st.mtime[0] == 3000, &alloc::format!("{}: UTIME_OMIT leaves atime alone", dir));
        let fd = open(&path, O_RDONLY);
        let now = [[0i64, UTIME_NOW], [0, UTIME_NOW]];
        c.check(sys4(SYS_UTIMENSAT, fd as u64, 0, now.as_ptr() as u64, 0) == 0, &alloc::format!("{}: futimens (NULL path, an fd): 0", dir));
        fstat(fd, &mut st);
        c.check(st.atime[0] > 1_000_000_000 && st.mtime[0] > 1_000_000_000, &alloc::format!("{}: UTIME_NOW, seen by fstat", dir));
        close(fd);
        unlink(&path);
    }
    let p = cstr("/tmp");
    let bad = [[0i64, 1_000_000_000], [0, 0]];
    c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, bad.as_ptr() as u64, 0) == -EINVAL, "utimensat with tv_nsec = 1e9: EINVAL");
    c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, p.as_ptr() as u64, 0, 0x8) == -EINVAL, "unknown flags: EINVAL");
    let h = cstr("/mnt/disk3/hello.txt");
    c.check(sys4(SYS_UTIMENSAT, AT_FDCWD as u64, h.as_ptr() as u64, 0, 0) == -EROFS, "utimensat on btrfs: EROFS");
    let mut st = Stat::default();
    stat("/mnt/disk3/hello.txt", &mut st);
    c.check(st.mtime[0] > 1_000_000_000, "btrfs files report their real mtime");
}

const SYS_PREAD64_NR: u64 = 17;

// --- misc --------------------------------------------------------------------

fn misc(c: &mut Checks) {
    // rlimits
    let mut lim = [0u64; 2];
    c.check(syscall3(SYS_GETRLIMIT, RLIMIT_STACK, lim.as_mut_ptr() as u64, 0) == 0 && lim[0] > 0 && lim[0] == lim[1], "RLIMIT_STACK: the real (fixed) stack size");
    c.check(syscall3(SYS_GETRLIMIT, 99, lim.as_mut_ptr() as u64, 0) == -EINVAL, "an unknown resource: EINVAL");
    let up = [64u64, 1000];
    c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, up.as_ptr() as u64, 0) == -EPERM, "raising NOFILE's hard limit past 64: EPERM");
    let inverted = [10u64, 5];
    c.check(syscall3(SYS_SETRLIMIT, RLIMIT_NOFILE, inverted.as_ptr() as u64, 0) == -EINVAL, "soft above hard: EINVAL");
    let new = [32u64, 64];
    let mut old = [0u64; 2];
    let r = sys4(SYS_PRLIMIT64, 0, RLIMIT_NOFILE, new.as_ptr() as u64, old.as_mut_ptr() as u64);
    c.check(r == 0 && old == [64, 64], "prlimit64 sets and returns the old value");
    let child = fork();
    if child == 0 {
        let mut l = [0u64; 2];
        syscall3(SYS_GETRLIMIT, RLIMIT_NOFILE, l.as_mut_ptr() as u64, 0);
        exit(if l == [32, 64] { 0 } else { 1 });
    }
    c.check(matches!(wait4(child, 0), Ok((_, s)) if wexitstatus(s) == 0), "fork inherits rlimits");
    let back = [64u64, 64];
    sys4(SYS_PRLIMIT64, getpid() as u64, RLIMIT_NOFILE, back.as_ptr() as u64, 0);
    c.check(sys4(SYS_PRLIMIT64, 99_999, RLIMIT_NOFILE, 0, old.as_mut_ptr() as u64) == -ESRCH, "prlimit64 of a missing pid: ESRCH");

    // prctl
    let name = cstr("libctest-worker-long");
    c.check(syscall3(SYS_PRCTL, 15, name.as_ptr() as u64, 0) == 0, "PR_SET_NAME");
    let mut got = [0xffu8; 16];
    syscall3(SYS_PRCTL, 16, got.as_mut_ptr() as u64, 0);
    c.check(&got[..15] == b"libctest-worker" && got[15] == 0, "PR_GET_NAME: cut to 15 bytes + NUL");
    let mut sig = -1i32;
    syscall3(SYS_PRCTL, 1, SIGTERM as u64, 0);
    syscall3(SYS_PRCTL, 2, &mut sig as *mut i32 as u64, 0);
    c.check(sig == SIGTERM, "PR_SET/GET_PDEATHSIG");
    c.check(syscall3(SYS_PRCTL, 1, 99, 0) == -EINVAL, "PR_SET_PDEATHSIG 99: EINVAL");
    syscall3(SYS_PRCTL, 1, 0, 0);
    c.check(syscall3(SYS_PRCTL, 3, 0, 0) == 1, "PR_GET_DUMPABLE: 1");
    c.check(syscall3(SYS_PRCTL, 4, 2, 0) == -EINVAL, "PR_SET_DUMPABLE 2: EINVAL");
    c.check(syscall5(SYS_PRCTL, 38, 1, 0, 0, 0) == 0 && syscall5(SYS_PRCTL, 39, 0, 0, 0, 0) == 1, "PR_SET/GET_NO_NEW_PRIVS");
    c.check(syscall3(SYS_PRCTL, 0x5555, 0, 0) == -EINVAL, "an unknown prctl: EINVAL");

    // sched_getaffinity: every CPU, returns the bytes written.
    let mut mask = [0u64; 2];
    let r = syscall3(SYS_SCHED_GETAFFINITY, 0, 16, mask.as_mut_ptr() as u64);
    let cpus = sysinfo().cpus as u32;
    c.check(r == 8 && mask[0].count_ones() == cpus && cpus >= 1, &alloc::format!("sched_getaffinity: {} CPU bits, returns 8", cpus));
    c.check(syscall3(SYS_SCHED_GETAFFINITY, 0, 4, mask.as_mut_ptr() as u64) == -EINVAL, "sched_getaffinity with 4 bytes: EINVAL");

    // madvise
    let page = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;
    unsafe { core::ptr::write_bytes(page as *mut u8, 0xaa, 8192) };
    c.check(syscall3(SYS_MADVISE, page, 8192, 3) == 0, "MADV_WILLNEED: 0 (a hint)");
    c.check(syscall3(SYS_MADVISE, page, 8192, 4) == 0, "MADV_DONTNEED: 0");
    let zero = unsafe { core::slice::from_raw_parts(page as *const u8, 8192) }.iter().all(|&b| b == 0);
    c.check(zero, "after MADV_DONTNEED private anonymous pages read as zeros");
    c.check(syscall3(SYS_MADVISE, page + 1, 4096, 4) == -EINVAL, "unaligned address: EINVAL");
    c.check(syscall3(SYS_MADVISE, page, 4096, 999) == -EINVAL, "unknown advice: EINVAL");
    munmap(page, 8192);
    c.check(syscall3(SYS_MADVISE, page, 4096, 4) == -ENOMEM, "on unmapped memory: ENOMEM");

    // Robust futex lists.
    let mut head = [0u64; 3];
    c.check(syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 23, 0) == -EINVAL, "set_robust_list with len 23: EINVAL");
    c.check(syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 24, 0) == 0, "set_robust_list");
    let (mut h, mut l) = (0u64, 0u64);
    syscall3(SYS_GET_ROBUST_LIST, 0, &mut h as *mut u64 as u64, &mut l as *mut u64 as u64);
    c.check(h == head.as_ptr() as u64 && l == 24, "get_robust_list returns it");
    syscall3(SYS_SET_ROBUST_LIST, 0, 24, 0);
    // A child dies holding a "robust mutex" in shared memory: the kernel
    // marks the lock word FUTEX_OWNER_DIED, so the next locker knows.
    let shared = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0) as u64;
    let word = shared as *mut u32;
    let entry = shared + 64; // the mutex's list node; lock word at entry - 64
    let child = fork();
    if child == 0 {
        let tid = syscall3(SYS_GETTID, 0, 0, 0) as u32;
        head[0] = entry; // list: head -> entry -> head
        head[1] = (-64i64) as u64; // futex_offset: from the node to the word
        head[2] = 0;
        unsafe {
            *(entry as *mut u64) = head.as_ptr() as u64;
            word.write_volatile(tid); // "locked by me"
        }
        syscall3(SYS_SET_ROBUST_LIST, head.as_ptr() as u64, 24, 0);
        exit(0); // ... and die holding it
    }
    wait(child);
    let w = unsafe { word.read_volatile() };
    c.check(w & FUTEX_OWNER_DIED != 0 && w & 0x3fff_ffff == 0, "a robust lock held by a dead thread: FUTEX_OWNER_DIED, owner 0");
    munmap(shared, 4096);
    head[0] = 0;
}

// --- orphans -----------------------------------------------------------------

fn orphans(c: &mut Checks) {
    // A child that leaves a running grandchild and a zombie grandchild
    // behind and exits without waiting for either.
    let child = fork();
    if child == 0 {
        if fork() == 0 {
            exit(0); // becomes a zombie: its parent never waits
        }
        if fork() == 0 {
            sleep_ms(40); // still running when its parent exits
            exit(0);
        }
        sleep_ms(10);
        exit(0);
    }
    c.check(matches!(wait4(child, 0), Ok((p, _)) if p == child), "the child exits without waiting for its own children");

    // With PR_SET_CHILD_SUBREAPER we adopt the orphan and can wait for it.
    c.check(syscall3(SYS_PRCTL, 36, 1, 0) == 0, "PR_SET_CHILD_SUBREAPER");
    let me = getpid();
    let child = fork();
    if child == 0 {
        if fork() == 0 {
            sleep_ms(40);
            exit(if getppid() == me { 5 } else { 6 }); // adopted by the subreaper?
        }
        exit(0);
    }
    c.check(matches!(wait4(child, 0), Ok((p, s)) if p == child && wexitstatus(s) == 0), "the middle process exits");
    let r = wait4(-1, 0);
    c.check(matches!(r, Ok((p, s)) if p != child && wexitstatus(s) == 5), "the subreaper reaps the orphaned grandchild, which saw getppid() = subreaper");
    c.check(wait4(-1, WNOHANG) == Err(-ECHILD), "no children left");
    sleep_ms(80); // let the first orphan finish before we exit
}