Skip to main content

proot/
abi.rs

1//! Syscall ABIs a tracee may use.
2//!
3//! Handlers work in x86_64 terms: the numbers in `syscall::nr` are the
4//! canonical names of the operations.  Every other ABI is mapped onto them
5//! here, together with a [`Variant`] describing how its arguments or
6//! structure layouts differ.  A 32-bit i386 process on an x86_64 kernel has
7//! different numbers, `ebx..ebp` argument registers, 4-byte pointers and a
8//! few 32-bit structures; aarch64 shares the layouts of x86_64 but uses the
9//! generic syscall table, which has no legacy `open`/`stat`/`access` family.
10
11use crate::sys::{AUDIT_ARCH_AARCH64, AUDIT_ARCH_ARM, AUDIT_ARCH_I386, AUDIT_ARCH_X86_64};
12use crate::syscall::nr::*;
13
14/// The ABI a request came through.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Abi {
17    X86_64,
18    I386,
19    Aarch64,
20    /// 32-bit ARM (EABI) compat on an aarch64 kernel.
21    Arm,
22}
23
24impl Abi {
25    /// ABI from a seccomp `arch` field; `None` for x32 and anything else.
26    pub fn from_audit(arch: u32) -> Option<Abi> {
27        match arch {
28            AUDIT_ARCH_X86_64 => Some(Abi::X86_64),
29            AUDIT_ARCH_I386 => Some(Abi::I386),
30            AUDIT_ARCH_AARCH64 => Some(Abi::Aarch64),
31            AUDIT_ARCH_ARM => Some(Abi::Arm),
32            _ => None,
33        }
34    }
35
36    /// The 64-bit ABI of the host this binary was built for.
37    pub fn native() -> Abi {
38        if cfg!(target_arch = "aarch64") {
39            Abi::Aarch64
40        } else {
41            Abi::X86_64
42        }
43    }
44
45    /// The 32-bit compat ABI of the host, if any.
46    pub fn compat() -> Abi {
47        if cfg!(target_arch = "aarch64") {
48            Abi::Arm
49        } else {
50            Abi::I386
51        }
52    }
53
54    /// Audit arch value the kernel reports for this ABI.
55    pub fn audit(self) -> u32 {
56        match self {
57            Abi::X86_64 => AUDIT_ARCH_X86_64,
58            Abi::I386 => AUDIT_ARCH_I386,
59            Abi::Aarch64 => AUDIT_ARCH_AARCH64,
60            Abi::Arm => AUDIT_ARCH_ARM,
61        }
62    }
63
64    /// Size of a pointer in the tracee.
65    pub fn ptr_size(self) -> usize {
66        match self {
67            Abi::X86_64 | Abi::Aarch64 => 8,
68            Abi::I386 | Abi::Arm => 4,
69        }
70    }
71
72    /// Maps a syscall number of this ABI onto the canonical (x86_64) number.
73    ///
74    /// # Arguments
75    /// * `nr` - Syscall number as the tracee issued it.
76    ///
77    /// # Returns
78    /// The canonical number and the argument variant, or `None` when the
79    /// syscall is not one we filter (or the ABI is not served).
80    pub fn to_canonical(self, nr: i64) -> Option<(i64, Variant)> {
81        match self {
82            Abi::X86_64 => Some((nr, Variant::Same)),
83            Abi::I386 => i386_to_x86_64(nr),
84            Abi::Aarch64 => aarch64_to_x86_64(nr).map(|n| (n, Variant::Same)),
85            Abi::Arm => arm_to_x86_64(nr),
86        }
87    }
88
89    /// This ABI's number for a canonical `open`-family syscall, with the index
90    /// of its flags argument (for the filter's `O_PATH` routing).
91    pub fn open_flags_arg(self, nr: i64) -> Option<usize> {
92        match (self, nr) {
93            (Abi::X86_64, OPEN) | (Abi::I386, 5) | (Abi::Arm, 5) => Some(1),
94            (Abi::X86_64, OPENAT) | (Abi::I386, 295) | (Abi::Aarch64, 56) | (Abi::Arm, 322) => Some(2),
95            _ => None,
96        }
97    }
98
99    /// Converts this ABI's `open(2)` flag bits into the host's.
100    ///
101    /// The generic values are shared by x86 and aarch64; ARM differs in
102    /// `O_DIRECTORY`, `O_NOFOLLOW`, `O_DIRECT` and `O_LARGEFILE`, so a 32-bit
103    /// ARM caller's flags are remapped before any handler interprets them.
104    ///
105    /// # Arguments
106    /// * `flags` - Flags as the tracee passed them.
107    pub fn open_flags_to_host(self, flags: i64) -> i64 {
108        match self {
109            Abi::Arm => {
110                const ARM_O_DIRECTORY: i64 = 0o40000;
111                const ARM_O_NOFOLLOW: i64 = 0o100000;
112                const ARM_O_DIRECT: i64 = 0o200000;
113                const ARM_O_LARGEFILE: i64 = 0o400000;
114                let mut out = flags & !(ARM_O_DIRECTORY | ARM_O_NOFOLLOW | ARM_O_DIRECT | ARM_O_LARGEFILE);
115                if flags & ARM_O_DIRECTORY != 0 {
116                    out |= libc::O_DIRECTORY as i64;
117                }
118                if flags & ARM_O_NOFOLLOW != 0 {
119                    out |= libc::O_NOFOLLOW as i64;
120                }
121                if flags & ARM_O_DIRECT != 0 {
122                    out |= libc::O_DIRECT as i64;
123                }
124                out
125            }
126            _ => flags,
127        }
128    }
129
130    /// The legacy `socketcall` multiplexer of this ABI, if it has one.
131    ///
132    /// i386 glibc issues every socket operation through it, so a 32-bit
133    /// program's `bind` and `connect` arrive here rather than as syscalls.
134    pub fn socketcall_nr(self) -> Option<i64> {
135        match self {
136            Abi::I386 => Some(102),
137            _ => None,
138        }
139    }
140
141    /// This ABI's numbers for every canonical syscall in `canonical`.
142    ///
143    /// # Arguments
144    /// * `canonical` - x86_64 numbers (the lists in `syscall`).
145    pub fn numbers_for(self, canonical: &[i64]) -> Vec<i64> {
146        match self {
147            Abi::X86_64 => canonical.to_vec(),
148            _ => (0..512)
149                .filter(|n| {
150                    self.to_canonical(*n)
151                        .map(|(x, _)| canonical.contains(&x))
152                        .unwrap_or(false)
153                })
154                .collect(),
155        }
156    }
157}
158
159/// Maps an aarch64 (generic table) syscall number onto the x86_64 number.
160///
161/// The generic table has only the `*at` forms; layouts match x86_64 except
162/// `struct stat`, which the handlers size from the host's `libc::stat`.
163pub fn aarch64_to_x86_64(nr: i64) -> Option<i64> {
164    Some(match nr {
165        5 => SETXATTR,
166        6 => LSETXATTR,
167        8 => GETXATTR,
168        9 => LGETXATTR,
169        11 => LISTXATTR,
170        12 => LLISTXATTR,
171        14 => REMOVEXATTR,
172        15 => LREMOVEXATTR,
173        17 => GETCWD,
174        27 => INOTIFY_ADD_WATCH,
175        33 => MKNODAT,
176        34 => MKDIRAT,
177        35 => UNLINKAT,
178        36 => SYMLINKAT,
179        37 => LINKAT,
180        38 => RENAMEAT,
181        43 => STATFS,
182        45 => TRUNCATE,
183        48 => FACCESSAT,
184        49 => CHDIR,
185        50 => FCHDIR,
186        97 => UNSHARE,
187        51 => CHROOT,
188        53 => FCHMODAT,
189        54 => FCHOWNAT,
190        56 => OPENAT,
191        78 => READLINKAT,
192        79 => NEWFSTATAT,
193        88 => UTIMENSAT,
194        160 => UNAME,
195        167 => PRCTL,
196        200 => BIND,
197        203 => CONNECT,
198        220 => CLONE,
199        268 => SETNS,
200        221 => EXECVE,
201        264 => NAME_TO_HANDLE_AT,
202        276 => RENAMEAT2,
203        281 => EXECVEAT,
204        435 => CLONE3,
205        291 => STATX,
206        437 => OPENAT2,
207        439 => FACCESSAT2,
208        452 => FCHMODAT2,
209        55 => FCHOWN,
210        91 => CAPSET,
211        143 => SETREGID,
212        144 => SETGID,
213        145 => SETREUID,
214        146 => SETUID,
215        147 => SETRESUID,
216        148 => GETRESUID,
217        149 => SETRESGID,
218        150 => GETRESGID,
219        151 => SETFSUID,
220        152 => SETFSGID,
221        158 => GETGROUPS,
222        159 => SETGROUPS,
223        174 => GETUID,
224        175 => GETEUID,
225        176 => GETGID,
226        177 => GETEGID,
227        _ => return None,
228    })
229}
230
231/// How a syscall's arguments differ from the x86_64 form it maps to.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum Variant {
234    /// Same arguments and layouts as the x86_64 number.
235    Same,
236    /// `stat`/`lstat`/`fstatat` writing the i386 `struct stat64` (96 bytes).
237    Stat64,
238    /// `stat`/`lstat` writing the old i386 `struct stat` (64 bytes).
239    OldStat,
240    /// `statfs64(path, size, buf)` writing the i386 `struct statfs64` (84 bytes).
241    Statfs64,
242    /// `statfs(path, buf)` writing the old i386 `struct statfs` (64 bytes).
243    OldStatfs,
244    /// `truncate64(path, len_lo, len_hi)` (i386).
245    Truncate64,
246    /// `truncate64(path, pad, len_lo, len_hi)`: ARM EABI pairs 64-bit arguments on even registers.
247    Truncate64Arm,
248    /// `stat64`/`lstat64`/`fstatat64` writing the ARM EABI `struct stat64` (104 bytes).
249    ArmStat64,
250    /// `getresuid`/`getresgid` with 16-bit ids (the pre-`32` i386 and ARM forms).
251    Id16,
252    /// `utimensat` with 32-bit `struct timespec` pairs.
253    Timespec32,
254    /// `utime`/`utimes` with 32-bit `time_t` structures.
255    Time32,
256}
257
258/// Maps an i386 syscall number onto the x86_64 number handlers understand.
259///
260/// # Arguments
261/// * `nr` - i386 syscall number.
262///
263/// # Returns
264/// The x86_64 number plus how its arguments differ, or `None` when the
265/// syscall is not one we filter.
266pub fn i386_to_x86_64(nr: i64) -> Option<(i64, Variant)> {
267    use Variant::*;
268    Some(match nr {
269        5 => (OPEN, Same),
270        8 => (CREAT, Same),
271        9 => (LINK, Same),
272        10 => (UNLINK, Same),
273        11 => (EXECVE, Same),
274        2 => (FORK, Same),
275        120 => (CLONE, Same),
276        133 => (FCHDIR, Same),
277        190 => (VFORK, Same),
278        310 => (UNSHARE, Same),
279        346 => (SETNS, Same),
280        435 => (CLONE3, Same),
281        12 => (CHDIR, Same),
282        14 => (MKNOD, Same),
283        15 => (CHMOD, Same),
284        16 => (LCHOWN, Same),
285        30 => (UTIME, Time32),
286        33 => (ACCESS, Same),
287        38 => (RENAME, Same),
288        39 => (MKDIR, Same),
289        40 => (RMDIR, Same),
290        61 => (CHROOT, Same),
291        83 => (SYMLINK, Same),
292        85 => (READLINK, Same),
293        92 => (TRUNCATE, Same),
294        99 => (STATFS, OldStatfs),
295        106 => (STAT, OldStat),
296        107 => (LSTAT, OldStat),
297        122 => (UNAME, Same),
298        172 => (PRCTL, Same),
299        182 => (CHOWN, Same),
300        183 => (GETCWD, Same),
301        193 => (TRUNCATE, Truncate64),
302        195 => (STAT, Stat64),
303        196 => (LSTAT, Stat64),
304        198 => (LCHOWN, Same),
305        212 => (CHOWN, Same),
306        226 => (SETXATTR, Same),
307        227 => (LSETXATTR, Same),
308        229 => (GETXATTR, Same),
309        230 => (LGETXATTR, Same),
310        232 => (LISTXATTR, Same),
311        233 => (LLISTXATTR, Same),
312        235 => (REMOVEXATTR, Same),
313        236 => (LREMOVEXATTR, Same),
314        268 => (STATFS, Statfs64),
315        271 => (UTIMES, Time32),
316        292 => (INOTIFY_ADD_WATCH, Same),
317        295 => (OPENAT, Same),
318        296 => (MKDIRAT, Same),
319        297 => (MKNODAT, Same),
320        298 => (FCHOWNAT, Same),
321        299 => (FUTIMESAT, Time32),
322        300 => (NEWFSTATAT, Stat64),
323        301 => (UNLINKAT, Same),
324        302 => (RENAMEAT, Same),
325        303 => (LINKAT, Same),
326        304 => (SYMLINKAT, Same),
327        305 => (READLINKAT, Same),
328        306 => (FCHMODAT, Same),
329        307 => (FACCESSAT, Same),
330        320 => (UTIMENSAT, Timespec32),
331        341 => (NAME_TO_HANDLE_AT, Same),
332        353 => (RENAMEAT2, Same),
333        358 => (EXECVEAT, Same),
334        361 => (BIND, Same),
335        362 => (CONNECT, Same),
336        383 => (STATX, Same),
337        412 => (UTIMENSAT, Same),
338        437 => (OPENAT2, Same),
339        439 => (FACCESSAT2, Same),
340        452 => (FCHMODAT2, Same),
341        23 => (SETUID, Same),
342        24 => (GETUID, Same),
343        46 => (SETGID, Same),
344        47 => (GETGID, Same),
345        49 => (GETEUID, Same),
346        50 => (GETEGID, Same),
347        70 => (SETREUID, Same),
348        71 => (SETREGID, Same),
349        80 => (GETGROUPS, Id16),
350        81 => (SETGROUPS, Same),
351        95 => (FCHOWN, Same),
352        138 => (SETFSUID, Same),
353        139 => (SETFSGID, Same),
354        164 => (SETRESUID, Same),
355        165 => (GETRESUID, Id16),
356        170 => (SETRESGID, Same),
357        171 => (GETRESGID, Id16),
358        185 => (CAPSET, Same),
359        199 => (GETUID, Same),
360        200 => (GETGID, Same),
361        201 => (GETEUID, Same),
362        202 => (GETEGID, Same),
363        203 => (SETREUID, Same),
364        204 => (SETREGID, Same),
365        205 => (GETGROUPS, Same),
366        206 => (SETGROUPS, Same),
367        207 => (FCHOWN, Same),
368        208 => (SETRESUID, Same),
369        209 => (GETRESUID, Same),
370        210 => (SETRESGID, Same),
371        211 => (GETRESGID, Same),
372        213 => (SETUID, Same),
373        214 => (SETGID, Same),
374        215 => (SETFSUID, Same),
375        216 => (SETFSGID, Same),
376        _ => return None,
377    })
378}
379
380/// Maps an ARM EABI syscall number onto the x86_64 number.
381///
382/// Like i386 it keeps the legacy non-`at` calls and the 32-bit structure
383/// variants, but its `stat64` layout and 64-bit argument pairing are its own.
384pub fn arm_to_x86_64(nr: i64) -> Option<(i64, Variant)> {
385    use Variant::*;
386    Some(match nr {
387        5 => (OPEN, Same),
388        8 => (CREAT, Same),
389        9 => (LINK, Same),
390        10 => (UNLINK, Same),
391        11 => (EXECVE, Same),
392        2 => (FORK, Same),
393        120 => (CLONE, Same),
394        133 => (FCHDIR, Same),
395        190 => (VFORK, Same),
396        337 => (UNSHARE, Same),
397        375 => (SETNS, Same),
398        435 => (CLONE3, Same),
399        12 => (CHDIR, Same),
400        14 => (MKNOD, Same),
401        15 => (CHMOD, Same),
402        16 => (LCHOWN, Same),
403        33 => (ACCESS, Same),
404        38 => (RENAME, Same),
405        39 => (MKDIR, Same),
406        40 => (RMDIR, Same),
407        61 => (CHROOT, Same),
408        83 => (SYMLINK, Same),
409        85 => (READLINK, Same),
410        92 => (TRUNCATE, Same),
411        99 => (STATFS, OldStatfs),
412        106 => (STAT, OldStat),
413        107 => (LSTAT, OldStat),
414        122 => (UNAME, Same),
415        172 => (PRCTL, Same),
416        182 => (CHOWN, Same),
417        183 => (GETCWD, Same),
418        193 => (TRUNCATE, Truncate64Arm),
419        195 => (STAT, ArmStat64),
420        196 => (LSTAT, ArmStat64),
421        198 => (LCHOWN, Same),
422        212 => (CHOWN, Same),
423        226 => (SETXATTR, Same),
424        227 => (LSETXATTR, Same),
425        229 => (GETXATTR, Same),
426        230 => (LGETXATTR, Same),
427        232 => (LISTXATTR, Same),
428        233 => (LLISTXATTR, Same),
429        235 => (REMOVEXATTR, Same),
430        236 => (LREMOVEXATTR, Same),
431        266 => (STATFS, Statfs64),
432        269 => (UTIMES, Time32),
433        282 => (BIND, Same),
434        283 => (CONNECT, Same),
435        317 => (INOTIFY_ADD_WATCH, Same),
436        322 => (OPENAT, Same),
437        323 => (MKDIRAT, Same),
438        324 => (MKNODAT, Same),
439        325 => (FCHOWNAT, Same),
440        326 => (FUTIMESAT, Time32),
441        327 => (NEWFSTATAT, ArmStat64),
442        328 => (UNLINKAT, Same),
443        329 => (RENAMEAT, Same),
444        330 => (LINKAT, Same),
445        331 => (SYMLINKAT, Same),
446        332 => (READLINKAT, Same),
447        333 => (FCHMODAT, Same),
448        334 => (FACCESSAT, Same),
449        348 => (UTIMENSAT, Timespec32),
450        370 => (NAME_TO_HANDLE_AT, Same),
451        382 => (RENAMEAT2, Same),
452        387 => (EXECVEAT, Same),
453        397 => (STATX, Same),
454        412 => (UTIMENSAT, Same),
455        437 => (OPENAT2, Same),
456        439 => (FACCESSAT2, Same),
457        452 => (FCHMODAT2, Same),
458        23 => (SETUID, Same),
459        24 => (GETUID, Same),
460        46 => (SETGID, Same),
461        47 => (GETGID, Same),
462        49 => (GETEUID, Same),
463        50 => (GETEGID, Same),
464        70 => (SETREUID, Same),
465        71 => (SETREGID, Same),
466        80 => (GETGROUPS, Id16),
467        81 => (SETGROUPS, Same),
468        95 => (FCHOWN, Same),
469        138 => (SETFSUID, Same),
470        139 => (SETFSGID, Same),
471        164 => (SETRESUID, Same),
472        165 => (GETRESUID, Id16),
473        170 => (SETRESGID, Same),
474        171 => (GETRESGID, Id16),
475        185 => (CAPSET, Same),
476        199 => (GETUID, Same),
477        200 => (GETGID, Same),
478        201 => (GETEUID, Same),
479        202 => (GETEGID, Same),
480        203 => (SETREUID, Same),
481        204 => (SETREGID, Same),
482        205 => (GETGROUPS, Same),
483        206 => (SETGROUPS, Same),
484        207 => (FCHOWN, Same),
485        208 => (SETRESUID, Same),
486        209 => (GETRESUID, Same),
487        210 => (SETRESGID, Same),
488        211 => (GETRESGID, Same),
489        213 => (SETUID, Same),
490        214 => (SETGID, Same),
491        215 => (SETFSUID, Same),
492        216 => (SETFSGID, Same),
493        _ => return None,
494    })
495}
496
497/// Little-endian field writer for the 32-bit structure layouts.
498struct Out<const N: usize>([u8; N]);
499
500impl<const N: usize> Out<N> {
501    fn new() -> Self {
502        Out([0u8; N])
503    }
504    fn u64(&mut self, off: usize, v: u64) {
505        self.0[off..off + 8].copy_from_slice(&v.to_ne_bytes());
506    }
507    fn u32(&mut self, off: usize, v: u64) {
508        self.0[off..off + 4].copy_from_slice(&(v as u32).to_ne_bytes());
509    }
510    fn u16(&mut self, off: usize, v: u64) {
511        self.0[off..off + 2].copy_from_slice(&(v as u16).to_ne_bytes());
512    }
513}
514
515/// Converts the host's `struct stat` into the i386 `struct stat64` (96 bytes).
516///
517/// Layout: st_dev 0 (8), __pad0 8 (4), __st_ino 12 (4), st_mode 16, st_nlink 20,
518/// st_uid 24, st_gid 28, st_rdev 32 (8), __pad3 40 (4), st_size 44 (8),
519/// st_blksize 52 (4), st_blocks 56 (8), times 64..88 (6 x 4), st_ino 88 (8).
520// Field types differ between hosts (u32 on aarch64 where x86_64 has u64), so the casts stay.
521#[allow(clippy::unnecessary_cast)]
522pub fn stat_to_stat64(st: &libc::stat) -> [u8; 96] {
523    let mut o = Out::<96>::new();
524    o.u64(0, st.st_dev as u64);
525    o.u32(12, st.st_ino as u64);
526    o.u32(16, st.st_mode as u64);
527    o.u32(20, st.st_nlink as u64);
528    o.u32(24, st.st_uid as u64);
529    o.u32(28, st.st_gid as u64);
530    o.u64(32, st.st_rdev as u64);
531    o.u64(44, st.st_size as u64);
532    o.u32(52, st.st_blksize as u64);
533    o.u64(56, st.st_blocks as u64);
534    for (off, v) in [
535        (64usize, st.st_atime),
536        (68, st.st_atime_nsec),
537        (72, st.st_mtime),
538        (76, st.st_mtime_nsec),
539        (80, st.st_ctime),
540        (84, st.st_ctime_nsec),
541    ] {
542        o.u32(off, v as u64);
543    }
544    o.u64(88, st.st_ino as u64);
545    o.0
546}
547
548/// Converts the host's `struct stat` into the ARM EABI `struct stat64` (104 bytes).
549///
550/// Same fields as the i386 layout, but every 64-bit field sits on an 8-byte
551/// boundary: st_size 48, st_blksize 56, st_blocks 64, times 72..96, st_ino 96.
552// Field types differ between hosts (u32 on aarch64 where x86_64 has u64), so the casts stay.
553#[allow(clippy::unnecessary_cast)]
554pub fn stat_to_arm_stat64(st: &libc::stat) -> [u8; 104] {
555    let mut o = Out::<104>::new();
556    o.u64(0, st.st_dev as u64);
557    o.u32(12, st.st_ino as u64);
558    o.u32(16, st.st_mode as u64);
559    o.u32(20, st.st_nlink as u64);
560    o.u32(24, st.st_uid as u64);
561    o.u32(28, st.st_gid as u64);
562    o.u64(32, st.st_rdev as u64);
563    o.u64(48, st.st_size as u64);
564    o.u32(56, st.st_blksize as u64);
565    o.u64(64, st.st_blocks as u64);
566    for (off, v) in [
567        (72usize, st.st_atime),
568        (76, st.st_atime_nsec),
569        (80, st.st_mtime),
570        (84, st.st_mtime_nsec),
571        (88, st.st_ctime),
572        (92, st.st_ctime_nsec),
573    ] {
574        o.u32(off, v as u64);
575    }
576    o.u64(96, st.st_ino as u64);
577    o.0
578}
579
580/// Converts the host's `struct stat` into the old 32-bit `struct stat` (64 bytes), shared by i386 and ARM.
581///
582/// Fields that do not fit yield `EOVERFLOW`, as the kernel does.
583// Field types differ between hosts (u32 on aarch64 where x86_64 has u64), so the casts stay.
584#[allow(clippy::unnecessary_cast)]
585pub fn stat_to_old_stat(st: &libc::stat) -> Result<[u8; 64], i32> {
586    let (ino, size) = (st.st_ino as u64, st.st_size as i64);
587    if ino > u32::MAX as u64 || size > i32::MAX as i64 || size < i32::MIN as i64 {
588        return Err(libc::EOVERFLOW);
589    }
590    // st_dev 0 (4), st_ino 4 (4), st_mode 8 (2), st_nlink 10 (2), st_uid 12 (2),
591    // st_gid 14 (2), st_rdev 16 (4), st_size 20 (4), st_blksize 24 (4), st_blocks 28 (4),
592    // times 32..56 (6 x 4), unused 56..64.
593    let mut o = Out::<64>::new();
594    o.u32(0, old_encode_dev(st.st_dev as u64) as u64);
595    o.u32(4, ino);
596    o.u16(8, st.st_mode as u64);
597    o.u16(10, st.st_nlink as u64);
598    o.u16(12, st.st_uid as u64);
599    o.u16(14, st.st_gid as u64);
600    o.u32(16, old_encode_dev(st.st_rdev as u64) as u64);
601    o.u32(20, size as u64);
602    o.u32(24, st.st_blksize as u64);
603    o.u32(28, st.st_blocks as u64);
604    for (off, v) in [
605        (32usize, st.st_atime),
606        (36, st.st_atime_nsec),
607        (40, st.st_mtime),
608        (44, st.st_mtime_nsec),
609        (48, st.st_ctime),
610        (52, st.st_ctime_nsec),
611    ] {
612        o.u32(off, v as u64);
613    }
614    Ok(o.0)
615}
616
617/// Kernel `old_encode_dev`: 8-bit major and minor packed into 16 bits.
618fn old_encode_dev(dev: u64) -> u32 {
619    let major = ((dev >> 8) & 0xfff) as u32;
620    let minor = ((dev & 0xff) | ((dev >> 12) & 0xfff00)) as u32;
621    ((major & 0xff) << 8) | (minor & 0xff)
622}
623
624/// The host's `f_flags`, which the libc crate leaves inside `f_spare`.
625///
626/// Both 64-bit hosts lay `struct statfs` out identically: `f_flags` is the
627/// word at offset 80.
628fn statfs_flags(sf: &libc::statfs) -> u64 {
629    let p = sf as *const _ as *const u8;
630    let b = unsafe { std::slice::from_raw_parts(p.add(80), 8) };
631    u64::from_ne_bytes(b.try_into().unwrap())
632}
633
634/// Bytes of the host's `struct statfs` `f_fsid` field.
635fn fsid_bytes(sf: &libc::statfs) -> [u8; 8] {
636    let p = &sf.f_fsid as *const _ as *const u8;
637    let mut b = [0u8; 8];
638    b.copy_from_slice(unsafe { std::slice::from_raw_parts(p, 8) });
639    b
640}
641
642/// Converts the host's `struct statfs` into the 32-bit `struct statfs64` (84 bytes, i386 and ARM).
643///
644/// Layout: f_type 0 (4), f_bsize 4 (4), f_blocks 8 (8), f_bfree 16, f_bavail 24, f_files 32,
645/// f_ffree 40, f_fsid 48 (8), f_namelen 56 (4), f_frsize 60 (4), f_flags 64 (4), spare 68..84.
646// Field types differ between hosts (u32 on aarch64 where x86_64 has u64), so the casts stay.
647#[allow(clippy::unnecessary_cast)]
648pub fn statfs_to_statfs64(sf: &libc::statfs) -> [u8; 84] {
649    let mut o = Out::<84>::new();
650    o.u32(0, sf.f_type as u64);
651    o.u32(4, sf.f_bsize as u64);
652    o.u64(8, sf.f_blocks as u64);
653    o.u64(16, sf.f_bfree as u64);
654    o.u64(24, sf.f_bavail as u64);
655    o.u64(32, sf.f_files as u64);
656    o.u64(40, sf.f_ffree as u64);
657    o.0[48..56].copy_from_slice(&fsid_bytes(sf));
658    o.u32(56, sf.f_namelen as u64);
659    o.u32(60, sf.f_frsize as u64);
660    o.u32(64, statfs_flags(sf));
661    o.0
662}
663
664/// Converts the host's `struct statfs` into the old 32-bit `struct statfs` (64 bytes, all 32-bit fields).
665// Field types differ between hosts (u32 on aarch64 where x86_64 has u64), so the casts stay.
666#[allow(clippy::unnecessary_cast)]
667pub fn statfs_to_old_statfs(sf: &libc::statfs) -> [u8; 64] {
668    let mut o = Out::<64>::new();
669    for (i, v) in [
670        sf.f_type as u64,
671        sf.f_bsize as u64,
672        sf.f_blocks as u64,
673        sf.f_bfree as u64,
674        sf.f_bavail as u64,
675        sf.f_files as u64,
676        sf.f_ffree as u64,
677    ]
678    .into_iter()
679    .enumerate()
680    {
681        o.u32(i * 4, v);
682    }
683    o.0[28..36].copy_from_slice(&fsid_bytes(sf));
684    o.u32(36, sf.f_namelen as u64);
685    o.u32(40, sf.f_frsize as u64);
686    o.u32(44, statfs_flags(sf));
687    o.0
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693
694    #[test]
695    fn maps_common_numbers() {
696        assert_eq!(i386_to_x86_64(5), Some((OPEN, Variant::Same)));
697        assert_eq!(i386_to_x86_64(195), Some((STAT, Variant::Stat64)));
698        assert_eq!(i386_to_x86_64(300), Some((NEWFSTATAT, Variant::Stat64)));
699        assert_eq!(i386_to_x86_64(11), Some((EXECVE, Variant::Same)));
700        assert_eq!(i386_to_x86_64(1), None);
701        assert!(Abi::I386.numbers_for(&[OPEN, OPENAT]).contains(&295));
702        assert_eq!(
703            Abi::Aarch64.numbers_for(&[OPENAT, NEWFSTATAT, OPEN]),
704            vec![56, 79]
705        );
706        assert_eq!(Abi::Aarch64.to_canonical(221), Some((EXECVE, Variant::Same)));
707        assert_eq!(Abi::Arm.to_canonical(5), Some((OPEN, Variant::Same)));
708    }
709
710    #[test]
711    fn stat64_layouts_carry_key_fields() {
712        let mut st: libc::stat = unsafe { std::mem::zeroed() };
713        st.st_ino = 0x1122334455u64 as _;
714        st.st_mode = 0o100644;
715        st.st_size = 5_000_000_000i64 as _;
716        st.st_atime = 1_700_000_000 as _;
717        let s = stat_to_stat64(&st);
718        assert_eq!(u64::from_ne_bytes(s[88..96].try_into().unwrap()), 0x1122334455);
719        assert_eq!(u32::from_ne_bytes(s[16..20].try_into().unwrap()), 0o100644);
720        assert_eq!(u64::from_ne_bytes(s[44..52].try_into().unwrap()), 5_000_000_000);
721        assert_eq!(u32::from_ne_bytes(s[64..68].try_into().unwrap()), 1_700_000_000);
722        let a = stat_to_arm_stat64(&st);
723        assert_eq!(u64::from_ne_bytes(a[96..104].try_into().unwrap()), 0x1122334455);
724        assert_eq!(u64::from_ne_bytes(a[48..56].try_into().unwrap()), 5_000_000_000);
725        assert_eq!(u32::from_ne_bytes(a[72..76].try_into().unwrap()), 1_700_000_000);
726        assert_eq!(stat_to_old_stat(&st), Err(libc::EOVERFLOW));
727    }
728
729    #[test]
730    fn arm_numbers_and_flags() {
731        assert_eq!(Abi::Arm.to_canonical(322), Some((OPENAT, Variant::Same)));
732        assert_eq!(Abi::Arm.to_canonical(327), Some((NEWFSTATAT, Variant::ArmStat64)));
733        assert_eq!(Abi::Arm.to_canonical(283), Some((CONNECT, Variant::Same)));
734        assert!(Abi::Arm.numbers_for(&[OPENAT, STAT]).contains(&195));
735        let arm_nofollow_dir = 0o100000 | 0o40000 | 0o2;
736        let host = Abi::Arm.open_flags_to_host(arm_nofollow_dir);
737        assert_eq!(host & libc::O_NOFOLLOW as i64, libc::O_NOFOLLOW as i64);
738        assert_eq!(host & libc::O_DIRECTORY as i64, libc::O_DIRECTORY as i64);
739        assert_eq!(host & 0o2, 0o2);
740        assert_eq!(
741            Abi::X86_64.open_flags_to_host(libc::O_NOFOLLOW as i64),
742            libc::O_NOFOLLOW as i64
743        );
744    }
745}