Skip to main content

air_sys_syscall/
net.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Wrappers de la famille `net` — sockets synchrones.
6//!
7//! Cf. `docs/specs/layer-0/family-net.md`.
8//!
9//! Toutes les opérations de cette famille sont synchrones. Les variantes
10//! io_uring seront ajoutées au Temps 2b (ADR-022), en réutilisant les
11//! mêmes types.
12
13#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
14compile_error!("air-sys-syscall::net supporte uniquement x86_64 et aarch64 (ADR-014).");
15
16use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
17use alloc::ffi::CString;
18use alloc::vec::Vec;
19use core::ffi::CStr;
20use core::num::NonZeroI32;
21
22use air_sys_types::net::{
23    AcceptFlags, AcceptResult, IoSlice, IoSliceMut, IpMembershipV4, IpMembershipV6, Ipv4SocketAddr,
24    Ipv6SocketAddr, LingerOption, MessageFlags, ReceiveMessageRequest, ReceiveMessageResult,
25    SendMessageRequest, SendMessageResult, ShutdownMode, SocketAddr, SocketDomain, SocketType,
26    UnixCredentials, UnixSocketAddr,
27};
28use air_sys_types::process::Pid;
29use air_sys_types::{Errno, OwnedFd as AirOwnedFd, Timeval};
30
31// ─────────────────────────────────────────────────────────────────────────
32// Helper commun : conversion d'une valeur de retour syscall négative en Errno.
33// ─────────────────────────────────────────────────────────────────────────
34
35fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
36    debug_assert!(ret < 0 && ret > -4096);
37    #[allow(clippy::cast_possible_truncation)]
38    let raw = ret.wrapping_neg() as i32;
39    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
40    Errno::from_nonzero(nz)
41}
42
43// ─────────────────────────────────────────────────────────────────────────
44// Constantes socket
45// ─────────────────────────────────────────────────────────────────────────
46
47/// `SOCK_CLOEXEC` : ajouté systématiquement par les wrappers socket/socketpair/accept4.
48const SOCK_CLOEXEC: i32 = 0x0008_0000;
49
50/// `MSG_NOSIGNAL` : activé par défaut sur send/sendmsg.
51const MSG_NOSIGNAL: i32 = 0x4000;
52
53// ─────────────────────────────────────────────────────────────────────────
54// Constantes sockopt
55// ─────────────────────────────────────────────────────────────────────────
56
57const SOL_SOCKET: i32 = 1;
58const SO_KEEPALIVE: i32 = 9;
59const SO_REUSEADDR: i32 = 2;
60const SO_REUSEPORT: i32 = 15;
61const SO_SNDBUF: i32 = 7;
62const SO_RCVBUF: i32 = 8;
63const SO_ERROR: i32 = 4;
64const SO_LINGER: i32 = 13;
65const SO_PEERCRED: i32 = 17;
66const SO_BINDTODEVICE: i32 = 25;
67// Options à `timeval` : sur LP64 (x86_64/aarch64, cibles Air), `SO_RCVTIMEO`/
68// `SO_SNDTIMEO` valent les constantes « OLD » (le `time_t` y est déjà 64-bit),
69// avec un `struct timeval` de 16 octets. Cf. `Timeval` (air-sys-types).
70const SO_RCVTIMEO: i32 = 20;
71const SO_SNDTIMEO: i32 = 21;
72
73const IPPROTO_TCP: i32 = 6;
74const TCP_NODELAY: i32 = 1;
75const TCP_KEEPIDLE: i32 = 4;
76const TCP_KEEPINTVL: i32 = 5;
77const TCP_KEEPCNT: i32 = 6;
78
79const IPPROTO_IP: i32 = 0;
80const IP_TTL: i32 = 2;
81const IP_ADD_MEMBERSHIP: i32 = 35;
82const IP_DROP_MEMBERSHIP: i32 = 36;
83
84const IPPROTO_IPV6: i32 = 41;
85const IPV6_V6ONLY: i32 = 26;
86// `IPV6_JOIN_GROUP` / `IPV6_LEAVE_GROUP` (alias `IPV6_ADD/DROP_MEMBERSHIP`).
87const IPV6_ADD_MEMBERSHIP: i32 = 20;
88const IPV6_DROP_MEMBERSHIP: i32 = 21;
89
90/// Taille de `struct timeval` sur LP64 (deux `long` de 8 octets).
91const TIMEVAL_LEN: u32 = 16;
92/// Taille de `struct ip_mreq` (deux `struct in_addr`).
93const IP_MREQ_LEN: u32 = 8;
94/// Taille de `struct ipv6_mreq` (`in6_addr` + `int ifindex`).
95const IPV6_MREQ_LEN: u32 = 20;
96
97// Les tailles ABI ci-dessus sont **verrouillées** sur le layout `#[repr(C)]` des
98// types couche 0 : un dérapage casserait la compilation (assertions const).
99const _: () = assert!(core::mem::size_of::<Timeval>() == TIMEVAL_LEN as usize);
100const _: () = assert!(core::mem::size_of::<IpMembershipV4>() == IP_MREQ_LEN as usize);
101const _: () = assert!(core::mem::size_of::<IpMembershipV6>() == IPV6_MREQ_LEN as usize);
102
103// ─────────────────────────────────────────────────────────────────────────
104// Taille maximale de sockaddr (ss_maxsize POSIX)
105// ─────────────────────────────────────────────────────────────────────────
106
107pub(crate) const MAX_SOCKADDR_LEN: usize = 128;
108
109/// Buffer de stockage sockaddr brut aligné sur u64.
110#[repr(C, align(8))]
111#[derive(Debug)]
112pub(crate) struct RawSockaddrStorage {
113    pub(crate) bytes: [u8; MAX_SOCKADDR_LEN],
114}
115
116/// Convertit un `SocketAddr` Air en représentation kernel brute.
117/// Retourne `(buffer, longueur_effective)`.
118pub(crate) fn socket_addr_to_raw(address: &SocketAddr) -> (RawSockaddrStorage, u32) {
119    let mut storage = RawSockaddrStorage {
120        bytes: [0u8; MAX_SOCKADDR_LEN],
121    };
122    match address {
123        SocketAddr::Unix(unix) => {
124            // sa_family = AF_UNIX = 1 (LE)
125            storage.bytes[0..2].copy_from_slice(&1u16.to_ne_bytes());
126            match unix {
127                UnixSocketAddr::Path(path) => {
128                    let bytes = path.as_bytes();
129                    let length = bytes.len().min(107);
130                    storage.bytes[2..2_usize.saturating_add(length)]
131                        .copy_from_slice(&bytes[..length]);
132                    storage.bytes[2_usize.saturating_add(length)] = 0; // NUL terminateur
133                    // addrlen = sizeof(sa_family) + length + NUL
134                    #[allow(clippy::cast_possible_truncation)]
135                    let addrlen = (2_usize.saturating_add(length).saturating_add(1)) as u32;
136                    (storage, addrlen)
137                }
138                UnixSocketAddr::Abstract(name) => {
139                    // Premier octet = '\0' (marqueur abstrait), puis le nom
140                    storage.bytes[2] = 0;
141                    let length = name.len().min(107);
142                    storage.bytes[3..3_usize.saturating_add(length)]
143                        .copy_from_slice(&name[..length]);
144                    // addrlen = sizeof(sa_family) + 1 (NUL) + length
145                    #[allow(clippy::cast_possible_truncation)]
146                    let addrlen = (2_usize.saturating_add(1).saturating_add(length)) as u32;
147                    (storage, addrlen)
148                }
149                UnixSocketAddr::Unnamed => (storage, 2),
150            }
151        }
152        SocketAddr::Ipv4(ipv4) => {
153            // sa_family = AF_INET = 2
154            storage.bytes[0..2].copy_from_slice(&2u16.to_ne_bytes());
155            // port en big-endian
156            let port_be = ipv4.port.to_be();
157            storage.bytes[2..4].copy_from_slice(&port_be.to_ne_bytes());
158            storage.bytes[4..8].copy_from_slice(&ipv4.address);
159            // sin_zero reste à 0 (déjà initialisé)
160            (storage, 16)
161        }
162        SocketAddr::Ipv6(ipv6) => {
163            // sa_family = AF_INET6 = 10
164            storage.bytes[0..2].copy_from_slice(&10u16.to_ne_bytes());
165            let port_be = ipv6.port.to_be();
166            storage.bytes[2..4].copy_from_slice(&port_be.to_ne_bytes());
167            storage.bytes[4..8].copy_from_slice(&ipv6.flowinfo.to_ne_bytes());
168            storage.bytes[8..24].copy_from_slice(&ipv6.address);
169            storage.bytes[24..28].copy_from_slice(&ipv6.scope_id.to_ne_bytes());
170            (storage, 28)
171        }
172    }
173}
174
175/// Convertit un buffer sockaddr brut en `SocketAddr` Air.
176/// Retourne `None` si le buffer est trop court ou la famille inconnue.
177pub(crate) fn raw_to_socket_addr(buffer: &[u8], length: u32) -> Option<SocketAddr> {
178    if length < 2 {
179        return None;
180    }
181    let len_usize = length as usize;
182    if buffer.len() < len_usize {
183        return None;
184    }
185    let family = u16::from_ne_bytes([buffer[0], buffer[1]]);
186    match family {
187        1 => {
188            // AF_UNIX
189            if length == 2 {
190                return Some(SocketAddr::Unix(UnixSocketAddr::Unnamed));
191            }
192            if length < 3 {
193                return None;
194            }
195            let path_bytes = buffer.get(2..len_usize)?;
196            if path_bytes.first() == Some(&0) {
197                // Abstract : le premier octet est '\0', suivi du nom
198                let name = path_bytes.get(1..).unwrap_or(&[]).to_vec();
199                Some(SocketAddr::Unix(UnixSocketAddr::Abstract(name)))
200            } else {
201                // Path : NUL-terminé
202                let nul = path_bytes
203                    .iter()
204                    .position(|&b| b == 0)
205                    .unwrap_or(path_bytes.len());
206                let cstr = CString::new(&path_bytes[..nul]).ok()?;
207                Some(SocketAddr::Unix(UnixSocketAddr::Path(cstr)))
208            }
209        }
210        2 => {
211            // AF_INET
212            if length < 16 {
213                return None;
214            }
215            let port = u16::from_be_bytes([buffer[2], buffer[3]]);
216            let address = [buffer[4], buffer[5], buffer[6], buffer[7]];
217            Some(SocketAddr::Ipv4(Ipv4SocketAddr { address, port }))
218        }
219        10 => {
220            // AF_INET6
221            if length < 28 {
222                return None;
223            }
224            let port = u16::from_be_bytes([buffer[2], buffer[3]]);
225            let flowinfo = u32::from_ne_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]);
226            let mut address = [0u8; 16];
227            address.copy_from_slice(buffer.get(8..24)?);
228            let scope_id = u32::from_ne_bytes([buffer[24], buffer[25], buffer[26], buffer[27]]);
229            Some(SocketAddr::Ipv6(Ipv6SocketAddr {
230                address,
231                port,
232                flowinfo,
233                scope_id,
234            }))
235        }
236        _ => None,
237    }
238}
239
240// ─────────────────────────────────────────────────────────────────────────
241// Codec sockaddr PUBLIC pour la face libc (ADR-070, descellement additif couche 0
242// — cadre ADR-051). Fines expositions des codecs internes ci-dessus : une seule
243// autorité de layout sockaddr (kernel=bible), zéro duplication côté libc.
244// ─────────────────────────────────────────────────────────────────────────
245
246/// **Décode** un `sockaddr` kernel brut (octets + `socklen_t`) en [`SocketAddr`] typé.
247/// Exposition publique de [`raw_to_socket_addr`] pour la face **libc** : `bytes` est le
248/// buffer présenté par le C, `length` la longueur associée. Rend `None` si la famille
249/// est inconnue ou la longueur insuffisante — aucune présomption sur les octets
250/// (Principe 3), aucun `unsafe`.
251#[must_use]
252pub fn socket_addr_from_bytes(bytes: &[u8], length: u32) -> Option<SocketAddr> {
253    raw_to_socket_addr(bytes, length)
254}
255
256/// **Encode** un [`SocketAddr`] typé en `sockaddr` kernel brut dans `out`, rendant la
257/// **longueur effective** (`socklen_t`) écrite, ou `None` si `out` est trop court.
258/// Exposition publique de [`socket_addr_to_raw`] pour la face libc **sans** fuiter le
259/// type de stockage interne (copie bornée dans le buffer de l'appelant).
260#[must_use]
261pub fn socket_addr_to_bytes(address: &SocketAddr, out: &mut [u8]) -> Option<u32> {
262    let (storage, length) = socket_addr_to_raw(address);
263    let length_usize = usize::try_from(length).ok()?;
264    let source = storage.bytes.get(..length_usize)?;
265    let target = out.get_mut(..length_usize)?;
266    target.copy_from_slice(source);
267    Some(length)
268}
269
270// ─────────────────────────────────────────────────────────────────────────
271// Helpers cmsg (ancillaires SCM_RIGHTS)
272// ─────────────────────────────────────────────────────────────────────────
273
274/// Construit un buffer `cmsghdr` + données FDs pour `SCM_RIGHTS` (LP64).
275///
276/// Structure `cmsghdr` sur LP64 :
277/// - cmsg_len  : u64 (8 octets) — taille incluant le header
278/// - cmsg_level: i32 (4 octets) = SOL_SOCKET = 1
279/// - cmsg_type : i32 (4 octets) = SCM_RIGHTS = 1
280/// - données   : i32 * nfds
281pub(crate) fn build_scm_rights_cmsg(fds: &[BorrowedFd<'_>]) -> Vec<u8> {
282    if fds.is_empty() {
283        return Vec::new();
284    }
285    let data_len = fds.len().saturating_mul(4); // chaque fd = i32
286    let cmsg_len = 16_usize.saturating_add(data_len); // header 16 + data
287    let total = cmsg_len.saturating_add(7) & !7_usize; // alignement 8
288    let mut buffer = vec![0u8; total];
289    // cmsg_len en u64 (LP64)
290    #[allow(clippy::cast_possible_truncation)]
291    buffer[0..8].copy_from_slice(&(cmsg_len as u64).to_ne_bytes());
292    // cmsg_level = SOL_SOCKET = 1
293    buffer[8..12].copy_from_slice(&1i32.to_ne_bytes());
294    // cmsg_type = SCM_RIGHTS = 1
295    buffer[12..16].copy_from_slice(&1i32.to_ne_bytes());
296    // FDs
297    for (i, fd) in fds.iter().enumerate() {
298        let raw = fd.as_raw_fd();
299        let offset = 16_usize.saturating_add(i.saturating_mul(4));
300        buffer[offset..offset.saturating_add(4)].copy_from_slice(&raw.to_ne_bytes());
301    }
302    buffer
303}
304
305/// Parse un buffer cmsg et extrait les `OwnedFd` reçus via `SCM_RIGHTS`.
306pub(crate) fn parse_scm_rights(cmsg_buf: &[u8], cmsg_len: usize) -> Vec<OwnedFd> {
307    let mut fds = Vec::new();
308    let mut offset = 0_usize;
309    while offset.saturating_add(16) <= cmsg_len && offset.saturating_add(16) <= cmsg_buf.len() {
310        let len_bytes: [u8; 8] = match cmsg_buf[offset..offset.saturating_add(8)].try_into() {
311            Ok(b) => b,
312            Err(_) => break,
313        };
314        // MAX_SOCKADDR_LEN = 128 < usize::MAX sur toutes les cibles supportées (LP64).
315        // La troncature de u64 à usize est intentionnelle : les tailles cmsg sont
316        // bornées à quelques centaines d'octets en pratique.
317        #[allow(clippy::cast_possible_truncation)]
318        let length = u64::from_ne_bytes(len_bytes) as usize;
319        let level_bytes: [u8; 4] =
320            match cmsg_buf[offset.saturating_add(8)..offset.saturating_add(12)].try_into() {
321                Ok(b) => b,
322                Err(_) => break,
323            };
324        let level = i32::from_ne_bytes(level_bytes);
325        let ty_bytes: [u8; 4] =
326            match cmsg_buf[offset.saturating_add(12)..offset.saturating_add(16)].try_into() {
327                Ok(b) => b,
328                Err(_) => break,
329            };
330        let ty = i32::from_ne_bytes(ty_bytes);
331
332        // SOL_SOCKET=1, SCM_RIGHTS=1
333        if level == 1 && ty == 1 && length >= 16 {
334            let mut fd_off = offset.saturating_add(16);
335            let end = offset.saturating_add(length);
336            while fd_off.saturating_add(4) <= end && fd_off.saturating_add(4) <= cmsg_buf.len() {
337                let raw_bytes: [u8; 4] = match cmsg_buf[fd_off..fd_off.saturating_add(4)].try_into()
338                {
339                    Ok(b) => b,
340                    Err(_) => break,
341                };
342                let raw = i32::from_ne_bytes(raw_bytes);
343                // SAFETY: Le kernel a retourné des FDs valides dans le cmsg SCM_RIGHTS.
344                // Ces FDs nous appartiennent maintenant (le kernel en transfère l'ownership).
345                fds.push(unsafe { OwnedFd::from_raw_fd(raw) });
346                fd_off = fd_off.saturating_add(4);
347            }
348        }
349        let aligned = length.saturating_add(7) & !7_usize;
350        if aligned == 0 {
351            break;
352        }
353        offset = offset.saturating_add(aligned);
354    }
355    fds
356}
357
358// ─────────────────────────────────────────────────────────────────────────
359// Structure KernelMsghdr (LP64)
360// ─────────────────────────────────────────────────────────────────────────
361
362/// Représentation kernel de `struct msghdr` en mode LP64 (Linux x86_64/aarch64).
363///
364/// Les champs `size_t` font 8 octets ; on insère un padding explicite entre
365/// `msg_namelen` (u32) et `msg_iov` (u64) pour respecter l'alignement LP64.
366#[repr(C)]
367struct KernelMsghdr {
368    msg_name: u64,       // *mut void = u64 (NULL si pas d'adresse)
369    msg_namelen: u32,    // longueur de l'adresse
370    _pad: u32,           // alignement LP64
371    msg_iov: u64,        // *mut iovec
372    msg_iovlen: u64,     // size_t
373    msg_control: u64,    // *mut void (cmsg)
374    msg_controllen: u64, // size_t
375    msg_flags: i32,      // drapeaux retournés
376    _pad2: u32,
377}
378
379// ─────────────────────────────────────────────────────────────────────────
380// Helper getsockopt/setsockopt bas-niveau
381// ─────────────────────────────────────────────────────────────────────────
382
383/// Lit une option socket en entier (`i32`).
384fn getsockopt_i32(sock: BorrowedFd<'_>, level: i32, optname: i32) -> Result<i32, Errno> {
385    let mut val: i32 = 0;
386    let mut length: u32 = 4;
387    let val_ptr: *mut i32 = &mut val;
388    let len_ptr: *mut u32 = &mut length;
389    // SAFETY:
390    // - getsockopt(2) écrit `*length` octets (au maximum `*len_ptr`) dans `*val_ptr`.
391    // - val et length sont locaux, valides pour la durée du syscall.
392    // - sock est garanti ouvert par BorrowedFd.
393    let ret = unsafe {
394        raw_syscall_getsockopt(
395            sock.as_raw_fd(),
396            level,
397            optname,
398            val_ptr as u64,
399            len_ptr as u64,
400        )
401    };
402    if ret < 0 {
403        return Err(errno_from_negative_syscall_ret(ret));
404    }
405    Ok(val)
406}
407
408/// Définit une option socket en entier (`i32`).
409fn setsockopt_i32(sock: BorrowedFd<'_>, level: i32, optname: i32, val: i32) -> Result<(), Errno> {
410    let val_ref: &i32 = &val;
411    let val_ptr: *const i32 = val_ref;
412    let length: u32 = 4;
413    // SAFETY:
414    // - setsockopt(2) lit `length` octets depuis `*val_ptr`.
415    // - val est local, valide pour la durée du syscall.
416    // - sock est garanti ouvert par BorrowedFd.
417    let ret =
418        unsafe { raw_syscall_setsockopt(sock.as_raw_fd(), level, optname, val_ptr as u64, length) };
419    if ret < 0 {
420        return Err(errno_from_negative_syscall_ret(ret));
421    }
422    Ok(())
423}
424
425// ─────────────────────────────────────────────────────────────────────────
426// setsockopt/getsockopt PUBLICS à valeur entière pour la face libc (ADR-071,
427// descellement additif couche 0 — cadre ADR-051). La quasi-totalité des options
428// (SO_REUSEADDR/TCP_NODELAY/SO_KEEPALIVE/SO_ERROR/IP_TOS/…) sont des `int`.
429// ─────────────────────────────────────────────────────────────────────────
430
431/// **Définit** une option socket à valeur **entière** (`setsockopt` avec `optlen == 4`).
432/// Exposition publique de [`setsockopt_i32`] pour la face libc : `level`/`optname` sont
433/// les entiers bruts fournis par le C (validés par le kernel).
434///
435/// # Errors
436/// L'`errno` de `setsockopt(2)` (`EBADF`/`ENOPROTOOPT`/`EINVAL`…), propagé.
437pub fn set_socket_option_int(
438    sock: BorrowedFd<'_>,
439    level: i32,
440    optname: i32,
441    value: i32,
442) -> Result<(), Errno> {
443    setsockopt_i32(sock, level, optname, value)
444}
445
446/// **Lit** une option socket à valeur **entière** (`getsockopt`). Exposition publique
447/// de [`getsockopt_i32`] pour la face libc.
448///
449/// # Errors
450/// L'`errno` de `getsockopt(2)`, propagé.
451pub fn get_socket_option_int(sock: BorrowedFd<'_>, level: i32, optname: i32) -> Result<i32, Errno> {
452    getsockopt_i32(sock, level, optname)
453}
454
455// ─────────────────────────────────────────────────────────────────────────
456// Setup des sockets
457// ─────────────────────────────────────────────────────────────────────────
458
459/// Crée un socket.
460///
461/// Wrappeur de `socket(2)`. `SOCK_CLOEXEC` est ajouté automatiquement
462/// par le wrapper pour garantir que le FD ne fuit pas vers les processus
463/// fils via `exec`.
464///
465/// # Parameters
466///
467/// - `domain` : famille de protocole ([`SocketDomain::Unix`] pour l'IPC
468///   Air via Conduit).
469/// - `ty` : type de socket ([`SocketType::Stream`] pour TCP/Unix stream).
470/// - `protocol` : protocole (0 = défaut pour le type).
471///
472/// # Errors
473///
474/// - `EAFNOSUPPORT` : famille de protocole non supportée.
475/// - `EPROTONOSUPPORT` : protocole non supporté pour ce type.
476/// - `EMFILE` : quota de FD du processus atteint.
477/// - `ENFILE` : quota de FD système atteint.
478/// - `ENOMEM` : mémoire kernel insuffisante.
479///
480/// # Examples
481///
482/// ```no_run
483/// use air_sys_syscall::net::socket;
484/// use air_sys_types::net::{SocketDomain, SocketType};
485///
486/// let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
487/// ```
488pub fn socket(domain: SocketDomain, ty: SocketType, protocol: i32) -> Result<AirOwnedFd, Errno> {
489    let domain_raw = domain as i32;
490    let ty_raw = (ty as i32) | SOCK_CLOEXEC;
491    // SAFETY:
492    // - socket(2) ne touche à aucune mémoire utilisateur (args entiers).
493    // - En cas de succès, retourne un FD valide >= 0 ; en cas d'erreur, ret < 0.
494    // - SOCK_CLOEXEC est setté atomiquement par le kernel.
495    let ret = unsafe { raw_syscall_socket(domain_raw, ty_raw, protocol) };
496    if ret < 0 {
497        return Err(errno_from_negative_syscall_ret(ret));
498    }
499    #[allow(clippy::cast_possible_truncation)]
500    let raw_fd = ret as i32;
501    // SAFETY: Le kernel a retourné un FD valide >= 0.
502    Ok(unsafe { AirOwnedFd::from_raw_fd(raw_fd) })
503}
504
505/// Lie un socket à une adresse locale.
506///
507/// Wrappeur de `bind(2)`. Pour les sockets Unix path-based, supprimer
508/// le chemin existant avec `unlinkat` (en ignorant `ENOENT`) avant
509/// d'appeler `bind` est la convention recommandée.
510///
511/// # Parameters
512///
513/// - `sock` : FD du socket.
514/// - `address` : adresse locale à lier.
515///
516/// # Errors
517///
518/// - `EADDRINUSE` : adresse déjà utilisée.
519/// - `EADDRNOTAVAIL` : adresse non disponible sur cet hôte.
520/// - `EINVAL` : socket déjà lié ou adresse invalide.
521/// - `EACCES` : permission refusée (ex. bind sur un port < 1024 sans
522///   privilèges).
523///
524/// # Examples
525///
526/// ```no_run
527/// use air_sys_syscall::net::{socket, bind};
528/// use air_sys_types::net::{SocketAddr, SocketDomain, SocketType, UnixSocketAddr};
529/// use air_sys_types::fd::AsFd;
530///
531/// let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
532/// bind(fd.as_fd(), &SocketAddr::Unix(UnixSocketAddr::Path(
533///     std::ffi::CString::new("/tmp/air.sock").expect("cstring"),
534/// ))).expect("bind");
535/// ```
536pub fn bind(sock: BorrowedFd<'_>, address: &SocketAddr) -> Result<(), Errno> {
537    let (raw, addrlen) = socket_addr_to_raw(address);
538    let addr_ptr: *const u8 = raw.bytes.as_ptr();
539    // SAFETY:
540    // - bind(2) lit `addrlen` octets depuis `addr_ptr` (la sockaddr).
541    // - `raw` est local, valide pour la durée du syscall.
542    // - sock est garanti ouvert par BorrowedFd.
543    let ret = unsafe { raw_syscall_bind(sock.as_raw_fd(), addr_ptr as u64, addrlen) };
544    if ret < 0 {
545        return Err(errno_from_negative_syscall_ret(ret));
546    }
547    Ok(())
548}
549
550/// Met un socket en mode écoute.
551///
552/// Wrappeur de `listen(2)`.
553///
554/// # Parameters
555///
556/// - `sock` : FD du socket déjà lié avec [`bind`].
557/// - `backlog` : taille de la file des connexions en attente d'`accept`.
558///
559/// # Errors
560///
561/// - `EADDRINUSE` : adresse déjà utilisée.
562/// - `EOPNOTSUPP` : type de socket ne supporte pas `listen`.
563///
564/// # Examples
565///
566/// ```no_run
567/// use air_sys_syscall::net::listen;
568/// use air_sys_types::fd::BorrowedFd;
569///
570/// # fn example(fd: BorrowedFd<'_>) {
571/// listen(fd, 128).expect("listen");
572/// # }
573/// ```
574pub fn listen(sock: BorrowedFd<'_>, backlog: u32) -> Result<(), Errno> {
575    // backlog est passé comme i32 au kernel ; saturer à i32::MAX si trop grand.
576    #[allow(clippy::cast_possible_truncation)]
577    let backlog_i32 = if backlog > i32::MAX as u32 {
578        i32::MAX
579    } else {
580        backlog as i32
581    };
582    // SAFETY:
583    // - listen(2) ne touche à aucune mémoire utilisateur.
584    // - sock est garanti ouvert par BorrowedFd.
585    let ret = unsafe { raw_syscall_listen(sock.as_raw_fd(), backlog_i32) };
586    if ret < 0 {
587        return Err(errno_from_negative_syscall_ret(ret));
588    }
589    Ok(())
590}
591
592// ─────────────────────────────────────────────────────────────────────────
593// Opérations connectées synchrones
594// ─────────────────────────────────────────────────────────────────────────
595
596/// Initie une connexion vers un pair.
597///
598/// Wrappeur de `connect(2)`. Pour les sockets non-bloquants, retourne
599/// immédiatement `EINPROGRESS` ; la connexion est terminée quand le
600/// socket devient writable (via `poll`/`epoll`/io_uring).
601///
602/// # Parameters
603///
604/// - `sock` : FD du socket.
605/// - `address` : adresse du pair.
606///
607/// # Errors
608///
609/// - `ECONNREFUSED` : le pair refuse la connexion (Unix : socket absent).
610/// - `EINPROGRESS` : connexion en cours (socket non-bloquant).
611/// - `EALREADY` : connexion déjà en cours.
612/// - `EISCONN` : socket déjà connecté.
613/// - `ETIMEDOUT` : expiration du délai de connexion.
614///
615/// # Examples
616///
617/// ```no_run
618/// use air_sys_syscall::net::{socket, connect};
619/// use air_sys_types::net::{SocketAddr, SocketDomain, SocketType, UnixSocketAddr};
620/// use air_sys_types::fd::AsFd;
621///
622/// let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
623/// connect(fd.as_fd(), &SocketAddr::Unix(UnixSocketAddr::Path(
624///     std::ffi::CString::new("/run/conduit.sock").expect("cstring"),
625/// ))).expect("connect");
626/// ```
627pub fn connect(sock: BorrowedFd<'_>, address: &SocketAddr) -> Result<(), Errno> {
628    let (raw, addrlen) = socket_addr_to_raw(address);
629    let addr_ptr: *const u8 = raw.bytes.as_ptr();
630    // SAFETY:
631    // - connect(2) lit `addrlen` octets depuis `addr_ptr`.
632    // - `raw` est local, valide pour la durée du syscall.
633    // - sock est garanti ouvert par BorrowedFd.
634    let ret = unsafe { raw_syscall_connect(sock.as_raw_fd(), addr_ptr as u64, addrlen) };
635    if ret < 0 {
636        return Err(errno_from_negative_syscall_ret(ret));
637    }
638    Ok(())
639}
640
641/// Accepte une connexion entrante.
642///
643/// Wrappeur de `accept4(2)` (préféré à `accept` car CLOEXEC atomique).
644/// Le wrapper active `AcceptFlags::CLOEXEC` par défaut.
645///
646/// # Parameters
647///
648/// - `listener` : FD du socket en écoute.
649/// - `flags` : [`AcceptFlags`] (CLOEXEC implicite).
650///
651/// # Errors
652///
653/// - `EAGAIN` / `EWOULDBLOCK` : pas de connexion disponible (non-bloquant).
654/// - `EINTR` : interrompu par un signal (ADR-021 convention 2).
655/// - `ECONNABORTED` : connexion abandonnée avant l'accept.
656///
657/// # Examples
658///
659/// ```no_run
660/// use air_sys_syscall::net::accept4;
661/// use air_sys_types::net::AcceptFlags;
662/// use air_sys_types::fd::BorrowedFd;
663///
664/// # fn example(listener: BorrowedFd<'_>) {
665/// let result = accept4(listener, AcceptFlags::CLOEXEC).expect("accept4");
666/// let _client_fd = result.fd;
667/// # }
668/// ```
669pub fn accept4(listener: BorrowedFd<'_>, flags: AcceptFlags) -> Result<AcceptResult, Errno> {
670    let mut addr_buf = RawSockaddrStorage {
671        bytes: [0u8; MAX_SOCKADDR_LEN],
672    };
673    // MAX_SOCKADDR_LEN = 128, valeur connue < u32::MAX : la troncature n'est pas possible.
674    #[allow(clippy::cast_possible_truncation)]
675    let mut addrlen: u32 = MAX_SOCKADDR_LEN as u32;
676    let addr_ptr: *mut u8 = addr_buf.bytes.as_mut_ptr();
677    let addrlen_ptr: *mut u32 = &mut addrlen;
678    // Forcer CLOEXEC même si l'appelant l'a oublié
679    let effective_flags = (flags | AcceptFlags::CLOEXEC).bits();
680    // SAFETY:
681    // - accept4(2) écrit l'adresse du pair dans `addr_ptr` (au plus `addrlen` octets)
682    //   et met à jour `*addrlen_ptr` avec la longueur réelle.
683    // - addr_buf et addrlen sont locaux, valides pour la durée du syscall.
684    // - En cas de succès, retourne un nouveau FD valide >= 0.
685    // - listener est garanti ouvert par BorrowedFd.
686    let ret = unsafe {
687        raw_syscall_accept4(
688            listener.as_raw_fd(),
689            addr_ptr as u64,
690            addrlen_ptr as u64,
691            effective_flags,
692        )
693    };
694    if ret < 0 {
695        return Err(errno_from_negative_syscall_ret(ret));
696    }
697    #[allow(clippy::cast_possible_truncation)]
698    let raw_fd = ret as i32;
699    // SAFETY: Le kernel a retourné un FD valide.
700    let fd = unsafe { AirOwnedFd::from_raw_fd(raw_fd) };
701    let address = raw_to_socket_addr(&addr_buf.bytes, addrlen);
702    Ok(AcceptResult { fd, address })
703}
704
705/// Envoie des données sur un socket connecté.
706///
707/// Wrappeur de `send(2)`. `MessageFlags::NOSIGNAL` est activé automatiquement
708/// pour éviter `SIGPIPE` sur les connexions fermées.
709///
710/// # Parameters
711///
712/// - `sock` : FD du socket connecté.
713/// - `buffer` : données à envoyer.
714/// - `flags` : drapeaux [`MessageFlags`].
715///
716/// # Errors
717///
718/// - `EAGAIN` : socket non-bloquant et buffer kernel plein.
719/// - `EINTR` : interrompu par un signal.
720/// - `EPIPE` / `ECONNRESET` : connexion fermée par le pair.
721/// - `EMSGSIZE` : message trop grand pour ce type de socket.
722///
723/// # Examples
724///
725/// ```no_run
726/// use air_sys_syscall::net::send;
727/// use air_sys_types::net::MessageFlags;
728/// use air_sys_types::fd::BorrowedFd;
729///
730/// # fn example(fd: BorrowedFd<'_>) {
731/// let n = send(fd, b"hello", MessageFlags::empty()).expect("send");
732/// # }
733/// ```
734pub fn send(sock: BorrowedFd<'_>, buffer: &[u8], flags: MessageFlags) -> Result<usize, Errno> {
735    let buf_ptr: *const u8 = buffer.as_ptr();
736    // MSG_NOSIGNAL ajouté automatiquement
737    #[allow(clippy::cast_sign_loss)]
738    let effective_flags = (flags.bits() | MSG_NOSIGNAL) as u64;
739    // SAFETY:
740    // - sendto(2) lit `buffer.len()` octets depuis `buf_ptr` (immutable borrow garanti).
741    // - Les args address/addrlen sont NULL/0 pour un socket connecté.
742    // - sock est garanti ouvert par BorrowedFd.
743    let ret = unsafe {
744        raw_syscall_sendto(
745            sock.as_raw_fd(),
746            buf_ptr as u64,
747            buffer.len(),
748            effective_flags,
749            0,
750            0,
751        )
752    };
753    if ret < 0 {
754        return Err(errno_from_negative_syscall_ret(ret));
755    }
756    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
757    Ok(ret as usize)
758}
759
760/// Reçoit des données d'un socket connecté.
761///
762/// Wrappeur de `recv(2)`. Retourne 0 si le pair a fermé la connexion
763/// proprement (EOF).
764///
765/// # Parameters
766///
767/// - `sock` : FD du socket.
768/// - `buffer` : buffer de destination.
769/// - `flags` : drapeaux [`MessageFlags`].
770///
771/// # Errors
772///
773/// - `EAGAIN` : pas de données disponibles (non-bloquant).
774/// - `EINTR` : interrompu par un signal.
775/// - `ECONNRESET` : connexion réinitialisée par le pair.
776///
777/// # Examples
778///
779/// ```no_run
780/// use air_sys_syscall::net::recv;
781/// use air_sys_types::net::MessageFlags;
782/// use air_sys_types::fd::BorrowedFd;
783///
784/// # fn example(fd: BorrowedFd<'_>) {
785/// let mut buffer = [0u8; 1024];
786/// let n = recv(fd, &mut buffer, MessageFlags::empty()).expect("recv");
787/// # }
788/// ```
789pub fn recv(sock: BorrowedFd<'_>, buffer: &mut [u8], flags: MessageFlags) -> Result<usize, Errno> {
790    let buf_ptr: *mut u8 = buffer.as_mut_ptr();
791    #[allow(clippy::cast_sign_loss)]
792    let flags_u64 = flags.bits() as u64;
793    // SAFETY:
794    // - recvfrom(2) écrit au plus `buffer.len()` octets dans `buf_ptr`.
795    // - buffer est local (mutable borrow exclusif), valide pour la durée du syscall.
796    // - Les args address/addrlen sont NULL/0 : on n'a pas besoin de l'adresse source.
797    // - sock est garanti ouvert par BorrowedFd.
798    let ret = unsafe {
799        raw_syscall_recvfrom(
800            sock.as_raw_fd(),
801            buf_ptr as u64,
802            buffer.len(),
803            flags_u64,
804            0,
805            0,
806        )
807    };
808    if ret < 0 {
809        return Err(errno_from_negative_syscall_ret(ret));
810    }
811    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
812    Ok(ret as usize)
813}
814
815/// Envoie un message avec données de contrôle (ancillaires).
816///
817/// Wrappeur de `sendmsg(2)`. Supporte l'envoi multi-buffer et le
818/// passage de FDs via `SCM_RIGHTS`. `NOSIGNAL` est activé par défaut.
819///
820/// # Parameters
821///
822/// - `sock` : FD du socket.
823/// - `request` : description du message à envoyer.
824///
825/// # Errors
826///
827/// Voir [`send`]. De plus :
828/// - `EINVAL` : nombre de FDs dans `request.fds` dépasse `SCM_MAX_FD`.
829///
830/// # Examples
831///
832/// ```no_run
833/// use air_sys_syscall::net::sendmsg;
834/// use air_sys_types::net::{IoSlice, MessageFlags, SendMessageRequest};
835/// use air_sys_types::fd::BorrowedFd;
836///
837/// # fn example(fd: BorrowedFd<'_>) {
838/// let data = b"hello";
839/// let iov = [IoSlice::new(data)];
840/// let req = SendMessageRequest {
841///     iov: &iov,
842///     address: None,
843///     fds: &[],
844///     flags: MessageFlags::empty(),
845/// };
846/// sendmsg(fd, &req).expect("sendmsg");
847/// # }
848/// ```
849pub fn sendmsg(
850    sock: BorrowedFd<'_>,
851    request: &SendMessageRequest<'_>,
852) -> Result<SendMessageResult, Errno> {
853    // Convertir l'adresse optionnelle
854    let (addr_storage, addr_storage_len) = if let Some(address) = request.address {
855        let (s, l) = socket_addr_to_raw(address);
856        (Some(s), l)
857    } else {
858        (None, 0u32)
859    };
860
861    // Construire le buffer cmsg (SCM_RIGHTS) si des FDs sont présents
862    let cmsg_buf = build_scm_rights_cmsg(request.fds);
863
864    let iov_ptr: *const IoSlice<'_> = request.iov.as_ptr();
865
866    let (name_ptr, name_len) = if let Some(ref s) = addr_storage {
867        (s.bytes.as_ptr() as u64, addr_storage_len)
868    } else {
869        (0u64, 0u32)
870    };
871
872    let (ctrl_ptr, ctrl_len) = if cmsg_buf.is_empty() {
873        (0u64, 0u64)
874    } else {
875        (cmsg_buf.as_ptr() as u64, cmsg_buf.len() as u64)
876    };
877
878    let mut msghdr = KernelMsghdr {
879        msg_name: name_ptr,
880        msg_namelen: name_len,
881        _pad: 0,
882        msg_iov: iov_ptr as u64,
883        msg_iovlen: request.iov.len() as u64,
884        msg_control: ctrl_ptr,
885        msg_controllen: ctrl_len,
886        msg_flags: 0,
887        _pad2: 0,
888    };
889
890    #[allow(clippy::cast_sign_loss)]
891    let effective_flags = (request.flags.bits() | MSG_NOSIGNAL) as u64;
892    let hdr_ptr: *mut KernelMsghdr = &mut msghdr;
893
894    // SAFETY:
895    // - sendmsg(2) lit la structure msghdr pointée par hdr_ptr et les buffers iov.
896    // - Tous les pointeurs (iov, cmsg, address) sont valides pour la durée du syscall,
897    //   garantis par les lifetimes de SendMessageRequest.
898    // - IoSlice est #[repr(C)] compatible avec struct iovec.
899    // - sock est garanti ouvert par BorrowedFd.
900    let ret = unsafe { raw_syscall_sendmsg(sock.as_raw_fd(), hdr_ptr as u64, effective_flags) };
901    if ret < 0 {
902        return Err(errno_from_negative_syscall_ret(ret));
903    }
904    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
905    Ok(SendMessageResult {
906        bytes_sent: ret as usize,
907    })
908}
909
910/// Reçoit un message avec données de contrôle (ancillaires).
911///
912/// Wrappeur de `recvmsg(2)`. Les FDs reçus via `SCM_RIGHTS` sont
913/// automatiquement encapsulés dans des [`OwnedFd`] avec `CLOEXEC`
914/// si [`MessageFlags::CMSG_CLOEXEC`] est dans `request.flags`.
915///
916/// # Parameters
917///
918/// - `sock` : FD du socket.
919/// - `request` : buffers de destination et flags.
920///
921/// # Errors
922///
923/// Voir [`recv`].
924///
925/// # Examples
926///
927/// ```no_run
928/// use air_sys_syscall::net::recvmsg;
929/// use air_sys_types::net::{IoSliceMut, MessageFlags, ReceiveMessageRequest};
930/// use air_sys_types::fd::BorrowedFd;
931///
932/// # fn example(fd: BorrowedFd<'_>) {
933/// let mut buffer = [0u8; 1024];
934/// let mut iov = [IoSliceMut::new(&mut buffer)];
935/// let mut req = ReceiveMessageRequest {
936///     iov: &mut iov,
937///     flags: MessageFlags::CMSG_CLOEXEC,
938/// };
939/// let result = recvmsg(fd, &mut req).expect("recvmsg");
940/// // result.fds contient les OwnedFd reçus via SCM_RIGHTS
941/// # }
942/// ```
943pub fn recvmsg(
944    sock: BorrowedFd<'_>,
945    request: &mut ReceiveMessageRequest<'_>,
946) -> Result<ReceiveMessageResult, Errno> {
947    // Buffer cmsg de 1 KiO pour recevoir les données de contrôle (FDs, creds…)
948    let mut cmsg_buf = [0u8; 1024];
949    let mut addr_buf = RawSockaddrStorage {
950        bytes: [0u8; MAX_SOCKADDR_LEN],
951    };
952    // MAX_SOCKADDR_LEN = 128, valeur connue < u32::MAX : la troncature n'est pas possible.
953    #[allow(clippy::cast_possible_truncation)]
954    let mut addrlen: u32 = MAX_SOCKADDR_LEN as u32;
955
956    let iov_ptr: *mut IoSliceMut<'_> = request.iov.as_mut_ptr();
957    let addr_ptr: *mut u8 = addr_buf.bytes.as_mut_ptr();
958    let addrlen_ptr: *mut u32 = &mut addrlen;
959    let cmsg_ptr: *mut u8 = cmsg_buf.as_mut_ptr();
960
961    let mut msghdr = KernelMsghdr {
962        msg_name: addr_ptr as u64,
963        msg_namelen: addrlen,
964        _pad: 0,
965        msg_iov: iov_ptr as u64,
966        msg_iovlen: request.iov.len() as u64,
967        msg_control: cmsg_ptr as u64,
968        msg_controllen: cmsg_buf.len() as u64,
969        msg_flags: 0,
970        _pad2: 0,
971    };
972
973    #[allow(clippy::cast_sign_loss)]
974    let flags_u64 = request.flags.bits() as u64;
975    let hdr_ptr: *mut KernelMsghdr = &mut msghdr;
976
977    // SAFETY:
978    // - recvmsg(2) écrit dans les buffers iov et cmsg, et met à jour msghdr.
979    // - Tous les buffers (iov, cmsg, address) sont locaux valides pour la durée du syscall.
980    // - IoSliceMut est #[repr(C)] compatible avec struct iovec.
981    // - Le kernel met à jour msg_namelen avec la longueur réelle de l'adresse.
982    // - sock est garanti ouvert par BorrowedFd.
983    // Note : on passe également addrlen_ptr mais en réalité le kernel met à jour
984    // msg_namelen dans la structure msghdr ; on le récupère après l'appel.
985    let ret = unsafe { raw_syscall_recvmsg(sock.as_raw_fd(), hdr_ptr as u64, flags_u64) };
986    if ret < 0 {
987        return Err(errno_from_negative_syscall_ret(ret));
988    }
989
990    // Lire msg_namelen mis à jour par le kernel
991    let returned_addrlen = msghdr.msg_namelen;
992    // Lire msg_controllen mis à jour par le kernel
993    #[allow(clippy::cast_possible_truncation)]
994    let returned_cmsg_len = msghdr.msg_controllen as usize;
995    // Lire msg_flags mis à jour par le kernel
996    let returned_flags_bits = msghdr.msg_flags;
997
998    let _ = addrlen_ptr; // addr_len mis à jour via msghdr.msg_namelen
999
1000    let address = raw_to_socket_addr(&addr_buf.bytes, returned_addrlen);
1001    let fds = parse_scm_rights(&cmsg_buf, returned_cmsg_len);
1002
1003    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1004    let bytes_received = ret as usize;
1005
1006    let flags = MessageFlags::from_bits_truncate(returned_flags_bits);
1007
1008    Ok(ReceiveMessageResult {
1009        bytes_received,
1010        address,
1011        fds,
1012        flags,
1013    })
1014}
1015
1016/// Ferme partiellement ou totalement un socket connecté.
1017///
1018/// Wrappeur de `shutdown(2)`. Différent de `close` : `shutdown` termine
1019/// la connexion de façon propre sans fermer le FD.
1020///
1021/// # Parameters
1022///
1023/// - `sock` : FD du socket.
1024/// - `mode` : direction(s) à fermer.
1025///
1026/// # Errors
1027///
1028/// - `ENOTCONN` : socket non connecté.
1029/// - `ENOTSOCK` : FD n'est pas un socket.
1030///
1031/// # Examples
1032///
1033/// ```no_run
1034/// use air_sys_syscall::net::shutdown;
1035/// use air_sys_types::net::ShutdownMode;
1036/// use air_sys_types::fd::BorrowedFd;
1037///
1038/// # fn example(fd: BorrowedFd<'_>) {
1039/// shutdown(fd, ShutdownMode::Write).expect("shutdown");
1040/// # }
1041/// ```
1042pub fn shutdown(sock: BorrowedFd<'_>, mode: ShutdownMode) -> Result<(), Errno> {
1043    let how = mode as i32;
1044    // SAFETY:
1045    // - shutdown(2) ne touche à aucune mémoire utilisateur.
1046    // - sock est garanti ouvert par BorrowedFd.
1047    let ret = unsafe { raw_syscall_shutdown(sock.as_raw_fd(), how) };
1048    if ret < 0 {
1049        return Err(errno_from_negative_syscall_ret(ret));
1050    }
1051    Ok(())
1052}
1053
1054// ─────────────────────────────────────────────────────────────────────────
1055// Opérations utilitaires
1056// ─────────────────────────────────────────────────────────────────────────
1057
1058/// Retourne l'adresse locale du socket.
1059///
1060/// # Errors
1061///
1062/// - `ENOTSOCK` : FD n'est pas un socket.
1063/// - `ENOBUFS` : mémoire système insuffisante.
1064///
1065/// # Examples
1066///
1067/// ```no_run
1068/// use air_sys_syscall::net::getsockname;
1069/// use air_sys_types::fd::BorrowedFd;
1070///
1071/// # fn example(fd: BorrowedFd<'_>) {
1072/// let address = getsockname(fd).expect("getsockname");
1073/// # }
1074/// ```
1075pub fn getsockname(sock: BorrowedFd<'_>) -> Result<SocketAddr, Errno> {
1076    let mut addr_buf = RawSockaddrStorage {
1077        bytes: [0u8; MAX_SOCKADDR_LEN],
1078    };
1079    // MAX_SOCKADDR_LEN = 128, valeur connue < u32::MAX : la troncature n'est pas possible.
1080    #[allow(clippy::cast_possible_truncation)]
1081    let mut addrlen: u32 = MAX_SOCKADDR_LEN as u32;
1082    let addr_ptr: *mut u8 = addr_buf.bytes.as_mut_ptr();
1083    let addrlen_ptr: *mut u32 = &mut addrlen;
1084    // SAFETY:
1085    // - getsockname(2) écrit l'adresse locale dans addr_ptr (au plus *addrlen_ptr octets)
1086    //   et met à jour *addrlen_ptr avec la longueur réelle.
1087    // - addr_buf et addrlen sont locaux, valides pour la durée du syscall.
1088    // - sock est garanti ouvert par BorrowedFd.
1089    let ret =
1090        unsafe { raw_syscall_getsockname(sock.as_raw_fd(), addr_ptr as u64, addrlen_ptr as u64) };
1091    if ret < 0 {
1092        return Err(errno_from_negative_syscall_ret(ret));
1093    }
1094    raw_to_socket_addr(&addr_buf.bytes, addrlen).ok_or_else(|| {
1095        // Famille inconnue ou buffer trop court : errno EAFNOSUPPORT
1096        #[allow(clippy::unwrap_used)]
1097        Errno::from_nonzero(NonZeroI32::new(97).unwrap()) // EAFNOSUPPORT = 97
1098    })
1099}
1100
1101/// Retourne l'adresse du pair connecté.
1102///
1103/// # Errors
1104///
1105/// - `ENOTCONN` : socket non connecté.
1106/// - `ENOTSOCK` : FD n'est pas un socket.
1107///
1108/// # Examples
1109///
1110/// ```no_run
1111/// use air_sys_syscall::net::getpeername;
1112/// use air_sys_types::fd::BorrowedFd;
1113///
1114/// # fn example(fd: BorrowedFd<'_>) {
1115/// let address = getpeername(fd).expect("getpeername");
1116/// # }
1117/// ```
1118pub fn getpeername(sock: BorrowedFd<'_>) -> Result<SocketAddr, Errno> {
1119    let mut addr_buf = RawSockaddrStorage {
1120        bytes: [0u8; MAX_SOCKADDR_LEN],
1121    };
1122    // MAX_SOCKADDR_LEN = 128, valeur connue < u32::MAX : la troncature n'est pas possible.
1123    #[allow(clippy::cast_possible_truncation)]
1124    let mut addrlen: u32 = MAX_SOCKADDR_LEN as u32;
1125    let addr_ptr: *mut u8 = addr_buf.bytes.as_mut_ptr();
1126    let addrlen_ptr: *mut u32 = &mut addrlen;
1127    // SAFETY:
1128    // - getpeername(2) écrit l'adresse du pair dans addr_ptr et met à jour *addrlen_ptr.
1129    // - addr_buf et addrlen sont locaux, valides pour la durée du syscall.
1130    // - sock est garanti ouvert par BorrowedFd.
1131    let ret =
1132        unsafe { raw_syscall_getpeername(sock.as_raw_fd(), addr_ptr as u64, addrlen_ptr as u64) };
1133    if ret < 0 {
1134        return Err(errno_from_negative_syscall_ret(ret));
1135    }
1136    // COUVERTURE (ADR-035, STRUCTURAL — cf. docs/COVERAGE-EXCEPTIONS.md, section
1137    // `net`). La closure `None` (`EAFNOSUPPORT`) est inatteignable par
1138    // construction : `getpeername(2)` réussit sur un socket créé via l'API safe,
1139    // dont le domaine ∈ {Unix(1), Ipv4(2), Ipv6(10)} (`SocketDomain`) — les trois
1140    // familles que `raw_to_socket_addr` décode toujours en `Some`. Les gardes de
1141    // troncature internes (`len < 16`/`< 28`) sont testées séparément.
1142    raw_to_socket_addr(&addr_buf.bytes, addrlen).ok_or_else(|| {
1143        #[allow(clippy::unwrap_used)]
1144        Errno::from_nonzero(NonZeroI32::new(97).unwrap()) // EAFNOSUPPORT
1145    })
1146}
1147
1148/// Crée une paire de sockets connectés.
1149///
1150/// Wrappeur de `socketpair(2)`. Équivalent à créer un pipe mais
1151/// pour les sockets Unix. Les deux FDs retournés sont symétriques.
1152///
1153/// Seuls les sockets Unix (`AF_UNIX`) supportent `socketpair` sur Linux.
1154/// `SOCK_CLOEXEC` est ajouté automatiquement.
1155///
1156/// # Parameters
1157///
1158/// - `domain` : doit être [`SocketDomain::Unix`] sur Linux.
1159/// - `ty` : type de socket.
1160/// - `protocol` : 0 = défaut.
1161///
1162/// # Errors
1163///
1164/// - `EAFNOSUPPORT` : domaine non supporté (non-Unix).
1165/// - `EMFILE`/`ENFILE` : quotas FD atteints.
1166///
1167/// # Examples
1168///
1169/// ```no_run
1170/// use air_sys_syscall::net::socketpair;
1171/// use air_sys_types::net::{SocketDomain, SocketType};
1172///
1173/// let (fd_a, fd_b) = socketpair(SocketDomain::Unix, SocketType::Stream, 0)
1174///     .expect("socketpair");
1175/// ```
1176pub fn socketpair(
1177    domain: SocketDomain,
1178    ty: SocketType,
1179    protocol: i32,
1180) -> Result<(AirOwnedFd, AirOwnedFd), Errno> {
1181    let mut sv = [0i32; 2];
1182    let sv_ptr: *mut i32 = sv.as_mut_ptr();
1183    let domain_raw = domain as i32;
1184    let ty_raw = (ty as i32) | SOCK_CLOEXEC;
1185    // SAFETY:
1186    // - socketpair(2) écrit deux FDs dans sv[0..2].
1187    // - sv est local, valide pour la durée du syscall.
1188    // - En cas de succès, sv[0] et sv[1] sont des FDs valides >= 0.
1189    let ret = unsafe { raw_syscall_socketpair(domain_raw, ty_raw, protocol, sv_ptr as u64) };
1190    if ret < 0 {
1191        return Err(errno_from_negative_syscall_ret(ret));
1192    }
1193    // SAFETY: Le kernel a retourné deux FDs valides dans sv.
1194    let fd_a = unsafe { AirOwnedFd::from_raw_fd(sv[0]) };
1195    // SAFETY: le noyau a écrit un second fd valide dans `sv[1]` ; Air en prend la propriété, une
1196    // seule fois.
1197    let fd_b = unsafe { AirOwnedFd::from_raw_fd(sv[1]) };
1198    Ok((fd_a, fd_b))
1199}
1200
1201// ─────────────────────────────────────────────────────────────────────────
1202// Options de socket (typées par option — ADR-021 convention 3)
1203// ─────────────────────────────────────────────────────────────────────────
1204
1205/// Lit l'option `SO_KEEPALIVE`.
1206pub fn get_so_keepalive(sock: BorrowedFd<'_>) -> Result<bool, Errno> {
1207    getsockopt_i32(sock, SOL_SOCKET, SO_KEEPALIVE).map(|v| v != 0)
1208}
1209
1210/// Définit l'option `SO_KEEPALIVE`.
1211pub fn set_so_keepalive(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno> {
1212    setsockopt_i32(sock, SOL_SOCKET, SO_KEEPALIVE, i32::from(enable))
1213}
1214
1215/// Lit l'option `SO_REUSEADDR`.
1216pub fn get_so_reuseaddr(sock: BorrowedFd<'_>) -> Result<bool, Errno> {
1217    getsockopt_i32(sock, SOL_SOCKET, SO_REUSEADDR).map(|v| v != 0)
1218}
1219
1220/// Définit l'option `SO_REUSEADDR`.
1221pub fn set_so_reuseaddr(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno> {
1222    setsockopt_i32(sock, SOL_SOCKET, SO_REUSEADDR, i32::from(enable))
1223}
1224
1225/// Lit l'option `SO_REUSEPORT`.
1226pub fn get_so_reuseport(sock: BorrowedFd<'_>) -> Result<bool, Errno> {
1227    getsockopt_i32(sock, SOL_SOCKET, SO_REUSEPORT).map(|v| v != 0)
1228}
1229
1230/// Définit l'option `SO_REUSEPORT`.
1231pub fn set_so_reuseport(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno> {
1232    setsockopt_i32(sock, SOL_SOCKET, SO_REUSEPORT, i32::from(enable))
1233}
1234
1235/// Lit l'option `TCP_NODELAY` (désactivation de l'algorithme Nagle).
1236pub fn get_tcp_nodelay(sock: BorrowedFd<'_>) -> Result<bool, Errno> {
1237    getsockopt_i32(sock, IPPROTO_TCP, TCP_NODELAY).map(|v| v != 0)
1238}
1239
1240/// Définit l'option `TCP_NODELAY`.
1241pub fn set_tcp_nodelay(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno> {
1242    setsockopt_i32(sock, IPPROTO_TCP, TCP_NODELAY, i32::from(enable))
1243}
1244
1245/// Lit la taille du buffer de réception (`SO_RCVBUF`).
1246pub fn get_so_rcvbuf(sock: BorrowedFd<'_>) -> Result<u32, Errno> {
1247    getsockopt_i32(sock, SOL_SOCKET, SO_RCVBUF).map(|v| {
1248        // Le kernel peut retourner une valeur positive ; on la traite comme u32.
1249        #[allow(clippy::cast_sign_loss)]
1250        let u = v as u32;
1251        u
1252    })
1253}
1254
1255/// Définit la taille du buffer de réception.
1256pub fn set_so_rcvbuf(sock: BorrowedFd<'_>, size: u32) -> Result<(), Errno> {
1257    // Le kernel interprète la valeur comme i32 ; saturer à i32::MAX si trop grand.
1258    #[allow(clippy::cast_possible_truncation)]
1259    let val = if size > i32::MAX as u32 {
1260        i32::MAX
1261    } else {
1262        size as i32
1263    };
1264    setsockopt_i32(sock, SOL_SOCKET, SO_RCVBUF, val)
1265}
1266
1267/// Lit la taille du buffer d'envoi (`SO_SNDBUF`).
1268pub fn get_so_sndbuf(sock: BorrowedFd<'_>) -> Result<u32, Errno> {
1269    getsockopt_i32(sock, SOL_SOCKET, SO_SNDBUF).map(|v| {
1270        #[allow(clippy::cast_sign_loss)]
1271        let u = v as u32;
1272        u
1273    })
1274}
1275
1276/// Définit la taille du buffer d'envoi.
1277pub fn set_so_sndbuf(sock: BorrowedFd<'_>, size: u32) -> Result<(), Errno> {
1278    #[allow(clippy::cast_possible_truncation)]
1279    let val = if size > i32::MAX as u32 {
1280        i32::MAX
1281    } else {
1282        size as i32
1283    };
1284    setsockopt_i32(sock, SOL_SOCKET, SO_SNDBUF, val)
1285}
1286
1287/// Lit l'erreur socket en attente (`SO_ERROR`) et l'efface.
1288///
1289/// Retourne `None` si aucune erreur n'est en attente.
1290pub fn get_so_error(sock: BorrowedFd<'_>) -> Result<Option<Errno>, Errno> {
1291    let val = getsockopt_i32(sock, SOL_SOCKET, SO_ERROR)?;
1292    if val == 0 {
1293        Ok(None)
1294    } else {
1295        let nz = NonZeroI32::new(val).ok_or_else(|| {
1296            // Ne se produit pas : val != 0 ici
1297            #[allow(clippy::unwrap_used)]
1298            Errno::from_nonzero(NonZeroI32::new(22).unwrap()) // EINVAL
1299        })?;
1300        Ok(Some(Errno::from_nonzero(nz)))
1301    }
1302}
1303
1304/// Lit les credentials du pair Unix (`SO_PEERCRED`).
1305///
1306/// Uniquement sur les sockets Unix (domain `AF_UNIX`).
1307pub fn get_so_peercred(sock: BorrowedFd<'_>) -> Result<UnixCredentials, Errno> {
1308    // struct ucred { pid_t pid; uid_t uid; gid_t gid; } = 3 * i32 = 12 octets
1309    let mut cred = [0i32; 3];
1310    let mut length: u32 = 12;
1311    let cred_ptr: *mut i32 = cred.as_mut_ptr();
1312    let len_ptr: *mut u32 = &mut length;
1313    // SAFETY:
1314    // - getsockopt(2) écrit la structure ucred (12 octets) dans cred.
1315    // - cred et length sont locaux, valides pour la durée du syscall.
1316    // - sock est garanti ouvert par BorrowedFd.
1317    let ret = unsafe {
1318        raw_syscall_getsockopt(
1319            sock.as_raw_fd(),
1320            SOL_SOCKET,
1321            SO_PEERCRED,
1322            cred_ptr as u64,
1323            len_ptr as u64,
1324        )
1325    };
1326    if ret < 0 {
1327        return Err(errno_from_negative_syscall_ret(ret));
1328    }
1329    // cred[0] = pid (i32), cred[1] = uid (u32), cred[2] = gid (u32)
1330    let pid_raw = cred[0];
1331    #[allow(clippy::cast_sign_loss)]
1332    let uid = cred[1] as u32;
1333    #[allow(clippy::cast_sign_loss)]
1334    let gid = cred[2] as u32;
1335    let nz = NonZeroI32::new(pid_raw).ok_or_else(|| {
1336        #[allow(clippy::unwrap_used)]
1337        Errno::from_nonzero(NonZeroI32::new(22).unwrap()) // EINVAL : pid=0 inattendu
1338    })?;
1339    Ok(UnixCredentials {
1340        pid: Pid::from_nonzero(nz),
1341        uid,
1342        gid,
1343    })
1344}
1345
1346/// Lit l'option `SO_LINGER`.
1347pub fn get_so_linger(sock: BorrowedFd<'_>) -> Result<LingerOption, Errno> {
1348    // struct linger { int l_onoff; int l_linger; } = 8 octets
1349    let mut linger = [0i32; 2];
1350    let mut length: u32 = 8;
1351    let linger_ptr: *mut i32 = linger.as_mut_ptr();
1352    let len_ptr: *mut u32 = &mut length;
1353    // SAFETY:
1354    // - getsockopt(2) écrit la structure linger (8 octets) dans linger.
1355    // - linger et length sont locaux, valides pour la durée du syscall.
1356    // - sock est garanti ouvert par BorrowedFd.
1357    let ret = unsafe {
1358        raw_syscall_getsockopt(
1359            sock.as_raw_fd(),
1360            SOL_SOCKET,
1361            SO_LINGER,
1362            linger_ptr as u64,
1363            len_ptr as u64,
1364        )
1365    };
1366    if ret < 0 {
1367        return Err(errno_from_negative_syscall_ret(ret));
1368    }
1369    if linger[0] == 0 {
1370        Ok(LingerOption::Disabled)
1371    } else {
1372        #[allow(clippy::cast_sign_loss)]
1373        let secs = linger[1] as u64;
1374        Ok(LingerOption::Enabled(core::time::Duration::from_secs(secs)))
1375    }
1376}
1377
1378/// Définit l'option `SO_LINGER`.
1379pub fn set_so_linger(sock: BorrowedFd<'_>, linger: LingerOption) -> Result<(), Errno> {
1380    let (l_onoff, l_linger): (i32, i32) = match linger {
1381        LingerOption::Disabled => (0, 0),
1382        LingerOption::Enabled(d) => {
1383            // Saturer à i32::MAX secondes (≈ 68 ans)
1384            let secs = d.as_secs();
1385            #[allow(clippy::cast_possible_truncation)]
1386            let secs_i32 = if secs > i32::MAX as u64 {
1387                i32::MAX
1388            } else {
1389                secs as i32
1390            };
1391            (1, secs_i32)
1392        }
1393    };
1394    let buffer = [l_onoff, l_linger];
1395    let buf_ptr: *const i32 = buffer.as_ptr();
1396    let length: u32 = 8;
1397    // SAFETY:
1398    // - setsockopt(2) lit 8 octets depuis buf_ptr (struct linger).
1399    // - buffer est local, valide pour la durée du syscall.
1400    // - sock est garanti ouvert par BorrowedFd.
1401    let ret = unsafe {
1402        raw_syscall_setsockopt(
1403            sock.as_raw_fd(),
1404            SOL_SOCKET,
1405            SO_LINGER,
1406            buf_ptr as u64,
1407            length,
1408        )
1409    };
1410    if ret < 0 {
1411        return Err(errno_from_negative_syscall_ret(ret));
1412    }
1413    Ok(())
1414}
1415
1416/// Lie le socket à une interface réseau (`SO_BINDTODEVICE`).
1417pub fn set_so_bindtodevice(sock: BorrowedFd<'_>, ifname: &CStr) -> Result<(), Errno> {
1418    let bytes = ifname.to_bytes_with_nul();
1419    let ptr: *const u8 = bytes.as_ptr();
1420    #[allow(clippy::cast_possible_truncation)]
1421    let length = bytes.len() as u32;
1422    // SAFETY:
1423    // - setsockopt(2) lit `length` octets depuis `ptr` (le nom d'interface NUL-terminé).
1424    // - Le CStr garantit la validité du pointeur et la présence du NUL terminateur.
1425    // - sock est garanti ouvert par BorrowedFd.
1426    let ret = unsafe {
1427        raw_syscall_setsockopt(
1428            sock.as_raw_fd(),
1429            SOL_SOCKET,
1430            SO_BINDTODEVICE,
1431            ptr as u64,
1432            length,
1433        )
1434    };
1435    if ret < 0 {
1436        return Err(errno_from_negative_syscall_ret(ret));
1437    }
1438    Ok(())
1439}
1440
1441/// Lit l'interface liée au socket.
1442pub fn get_so_bindtodevice(sock: BorrowedFd<'_>) -> Result<CString, Errno> {
1443    // IFNAMSIZ = 16 octets suffisent pour un nom d'interface Linux
1444    let mut buffer = [0u8; 16];
1445    let mut length: u32 = 16;
1446    let buf_ptr: *mut u8 = buffer.as_mut_ptr();
1447    let len_ptr: *mut u32 = &mut length;
1448    // SAFETY:
1449    // - getsockopt(2) écrit le nom de l'interface (NUL-terminé) dans buffer.
1450    // - buffer et length sont locaux, valides pour la durée du syscall.
1451    // - sock est garanti ouvert par BorrowedFd.
1452    let ret = unsafe {
1453        raw_syscall_getsockopt(
1454            sock.as_raw_fd(),
1455            SOL_SOCKET,
1456            SO_BINDTODEVICE,
1457            buf_ptr as u64,
1458            len_ptr as u64,
1459        )
1460    };
1461    if ret < 0 {
1462        return Err(errno_from_negative_syscall_ret(ret));
1463    }
1464    // Trouver le NUL terminateur pour construire le CString
1465    let nul_pos = buffer
1466        .iter()
1467        .position(|&b| b == 0)
1468        .unwrap_or(length as usize);
1469    // COUVERTURE (ADR-035, STRUCTURAL — cf. docs/COVERAGE-EXCEPTIONS.md, section
1470    // `net`). La closure `map_err` (`EINVAL`) est inatteignable par construction :
1471    // `nul_pos` est la position du **premier** `NUL` (ou `length` à défaut), donc
1472    // `buffer[..nul_pos]` ne contient aucun `NUL` interne → `CString::new`
1473    // retourne toujours `Ok`.
1474    CString::new(&buffer[..nul_pos]).map_err(|_| {
1475        #[allow(clippy::unwrap_used)]
1476        Errno::from_nonzero(NonZeroI32::new(22).unwrap()) // EINVAL
1477    })
1478}
1479
1480/// Définit le délai `TCP_KEEPIDLE` (secondes d'inactivité avant premier keepalive).
1481pub fn set_tcp_keepidle(sock: BorrowedFd<'_>, seconds: u32) -> Result<(), Errno> {
1482    #[allow(clippy::cast_possible_truncation)]
1483    let val = if seconds > i32::MAX as u32 {
1484        i32::MAX
1485    } else {
1486        seconds as i32
1487    };
1488    setsockopt_i32(sock, IPPROTO_TCP, TCP_KEEPIDLE, val)
1489}
1490
1491/// Définit le délai `TCP_KEEPINTVL` (secondes entre deux keepalives).
1492pub fn set_tcp_keepintvl(sock: BorrowedFd<'_>, seconds: u32) -> Result<(), Errno> {
1493    #[allow(clippy::cast_possible_truncation)]
1494    let val = if seconds > i32::MAX as u32 {
1495        i32::MAX
1496    } else {
1497        seconds as i32
1498    };
1499    setsockopt_i32(sock, IPPROTO_TCP, TCP_KEEPINTVL, val)
1500}
1501
1502/// Définit le nombre max de keepalives sans réponse (`TCP_KEEPCNT`).
1503pub fn set_tcp_keepcnt(sock: BorrowedFd<'_>, count: u32) -> Result<(), Errno> {
1504    #[allow(clippy::cast_possible_truncation)]
1505    let val = if count > i32::MAX as u32 {
1506        i32::MAX
1507    } else {
1508        count as i32
1509    };
1510    setsockopt_i32(sock, IPPROTO_TCP, TCP_KEEPCNT, val)
1511}
1512
1513/// Définit le TTL IP (`IP_TTL`).
1514pub fn set_ip_ttl(sock: BorrowedFd<'_>, ttl: u8) -> Result<(), Errno> {
1515    setsockopt_i32(sock, IPPROTO_IP, IP_TTL, i32::from(ttl))
1516}
1517
1518/// Restreint les sockets IPv6 aux connexions IPv6-only (`IPV6_V6ONLY`).
1519pub fn set_ipv6_v6only(sock: BorrowedFd<'_>, v6only: bool) -> Result<(), Errno> {
1520    setsockopt_i32(sock, IPPROTO_IPV6, IPV6_V6ONLY, i32::from(v6only))
1521}
1522
1523// ─────────────────────────────────────────────────────────────────────────
1524// Options à structure `timeval` — délais de réception / d'envoi
1525// (`SO_RCVTIMEO` / `SO_SNDTIMEO`). Options **structurées** (16 octets) : la
1526// couche 0 pose/lit un `Timeval` typé (jamais un `setsockopt` générique).
1527// ─────────────────────────────────────────────────────────────────────────
1528
1529/// Décode un `timeval` **rendu par `getsockopt`** en délai optionnel.
1530///
1531/// Fonction **pure** (aucun syscall) : le kernel écrit une structure `timeval`
1532/// que NOUS relisons ensuite. Un `timeval` nul (`{0, 0}`) signifie « aucun délai
1533/// armé » (le socket bloque indéfiniment) → `None` ; toute autre valeur →
1534/// `Some(timeval)`. Isolé pour être **property-testé** (cf. exigence fuzz du
1535/// décodage `getsockopt`).
1536fn decode_timeout(tv: Timeval) -> Option<Timeval> {
1537    if tv.tv_sec == 0 && tv.tv_usec == 0 {
1538        None
1539    } else {
1540        Some(tv)
1541    }
1542}
1543
1544/// Pose un délai (`SO_RCVTIMEO` / `SO_SNDTIMEO`) via un `timeval` de 16 octets.
1545///
1546/// `None` ⇒ `timeval` nul = « pas de délai » (sémantique kernel : blocage sans
1547/// limite). Helper **interne** paramétré par `optname` (les points d'entrée
1548/// publics restent dédiés, ADR-021 conv. 3).
1549fn set_socket_timeout(
1550    sock: BorrowedFd<'_>,
1551    optname: i32,
1552    timeout: Option<Timeval>,
1553) -> Result<(), Errno> {
1554    let tv = timeout.unwrap_or(Timeval {
1555        tv_sec: 0,
1556        tv_usec: 0,
1557    });
1558    let tv_ptr: *const Timeval = &tv;
1559    // SAFETY:
1560    // - setsockopt(2) lit `TIMEVAL_LEN` (16) octets depuis tv_ptr (struct timeval).
1561    // - tv est local, valide pour la durée du syscall ; layout #[repr(C)] fidèle
1562    //   (assertion const `size_of::<Timeval>() == 16`).
1563    // - sock est garanti ouvert par BorrowedFd.
1564    let ret = unsafe {
1565        raw_syscall_setsockopt(
1566            sock.as_raw_fd(),
1567            SOL_SOCKET,
1568            optname,
1569            tv_ptr as u64,
1570            TIMEVAL_LEN,
1571        )
1572    };
1573    if ret < 0 {
1574        return Err(errno_from_negative_syscall_ret(ret));
1575    }
1576    Ok(())
1577}
1578
1579/// Lit un délai (`SO_RCVTIMEO` / `SO_SNDTIMEO`). Helper **interne** paramétré par
1580/// `optname`.
1581fn get_socket_timeout(sock: BorrowedFd<'_>, optname: i32) -> Result<Option<Timeval>, Errno> {
1582    let mut tv = Timeval {
1583        tv_sec: 0,
1584        tv_usec: 0,
1585    };
1586    let mut length: u32 = TIMEVAL_LEN;
1587    let tv_ptr: *mut Timeval = &mut tv;
1588    let len_ptr: *mut u32 = &mut length;
1589    // SAFETY:
1590    // - getsockopt(2) écrit au plus `length` (16) octets dans tv (struct timeval).
1591    // - tv et length sont locaux, valides pour la durée du syscall ; layout
1592    //   #[repr(C)] fidèle (assertion const `size_of::<Timeval>() == 16`).
1593    // - sock est garanti ouvert par BorrowedFd.
1594    let ret = unsafe {
1595        raw_syscall_getsockopt(
1596            sock.as_raw_fd(),
1597            SOL_SOCKET,
1598            optname,
1599            tv_ptr as u64,
1600            len_ptr as u64,
1601        )
1602    };
1603    if ret < 0 {
1604        return Err(errno_from_negative_syscall_ret(ret));
1605    }
1606    Ok(decode_timeout(tv))
1607}
1608
1609/// Définit le délai de **réception** (`SO_RCVTIMEO`). `None` désarme le délai.
1610pub fn set_receive_timeout(sock: BorrowedFd<'_>, timeout: Option<Timeval>) -> Result<(), Errno> {
1611    set_socket_timeout(sock, SO_RCVTIMEO, timeout)
1612}
1613
1614/// Lit le délai de **réception** (`SO_RCVTIMEO`). `None` = aucun délai armé.
1615pub fn receive_timeout(sock: BorrowedFd<'_>) -> Result<Option<Timeval>, Errno> {
1616    get_socket_timeout(sock, SO_RCVTIMEO)
1617}
1618
1619/// Définit le délai d'**envoi** (`SO_SNDTIMEO`). `None` désarme le délai.
1620pub fn set_send_timeout(sock: BorrowedFd<'_>, timeout: Option<Timeval>) -> Result<(), Errno> {
1621    set_socket_timeout(sock, SO_SNDTIMEO, timeout)
1622}
1623
1624/// Lit le délai d'**envoi** (`SO_SNDTIMEO`). `None` = aucun délai armé.
1625pub fn send_timeout(sock: BorrowedFd<'_>) -> Result<Option<Timeval>, Errno> {
1626    get_socket_timeout(sock, SO_SNDTIMEO)
1627}
1628
1629// ─────────────────────────────────────────────────────────────────────────
1630// Appartenance à un groupe multicast — options **structurées**
1631// `ip_mreq` (v4) / `ipv6_mreq` (v6). Fonctions dédiées typées (ADR-021).
1632// ─────────────────────────────────────────────────────────────────────────
1633
1634/// Pose une option d'appartenance multicast **IPv4** (`ip_mreq`, 8 octets).
1635/// Helper **interne** paramétré par `optname` (`IP_ADD`/`IP_DROP_MEMBERSHIP`).
1636fn set_ip_membership_v4(
1637    sock: BorrowedFd<'_>,
1638    optname: i32,
1639    membership: &IpMembershipV4,
1640) -> Result<(), Errno> {
1641    let ptr: *const IpMembershipV4 = membership;
1642    // SAFETY:
1643    // - setsockopt(2) lit `IP_MREQ_LEN` (8) octets depuis ptr (struct ip_mreq).
1644    // - `membership` est valide pour la durée de l'appel (référence empruntée) ;
1645    //   layout #[repr(C)] fidèle (assertion const `size_of == 8`).
1646    // - sock est garanti ouvert par BorrowedFd.
1647    let ret = unsafe {
1648        raw_syscall_setsockopt(
1649            sock.as_raw_fd(),
1650            IPPROTO_IP,
1651            optname,
1652            ptr as u64,
1653            IP_MREQ_LEN,
1654        )
1655    };
1656    if ret < 0 {
1657        return Err(errno_from_negative_syscall_ret(ret));
1658    }
1659    Ok(())
1660}
1661
1662/// Pose une option d'appartenance multicast **IPv6** (`ipv6_mreq`, 20 octets).
1663/// Helper **interne** paramétré par `optname` (`IPV6_ADD`/`IPV6_DROP_MEMBERSHIP`).
1664fn set_ip_membership_v6(
1665    sock: BorrowedFd<'_>,
1666    optname: i32,
1667    membership: &IpMembershipV6,
1668) -> Result<(), Errno> {
1669    let ptr: *const IpMembershipV6 = membership;
1670    // SAFETY:
1671    // - setsockopt(2) lit `IPV6_MREQ_LEN` (20) octets depuis ptr (struct ipv6_mreq).
1672    // - `membership` est valide pour la durée de l'appel (référence empruntée) ;
1673    //   layout #[repr(C)] fidèle (assertion const `size_of == 20`).
1674    // - sock est garanti ouvert par BorrowedFd.
1675    let ret = unsafe {
1676        raw_syscall_setsockopt(
1677            sock.as_raw_fd(),
1678            IPPROTO_IPV6,
1679            optname,
1680            ptr as u64,
1681            IPV6_MREQ_LEN,
1682        )
1683    };
1684    if ret < 0 {
1685        return Err(errno_from_negative_syscall_ret(ret));
1686    }
1687    Ok(())
1688}
1689
1690/// **Rejoint** un groupe multicast IPv4 (`IP_ADD_MEMBERSHIP`, `struct ip_mreq`).
1691pub fn join_multicast_v4(sock: BorrowedFd<'_>, membership: &IpMembershipV4) -> Result<(), Errno> {
1692    set_ip_membership_v4(sock, IP_ADD_MEMBERSHIP, membership)
1693}
1694
1695/// **Quitte** un groupe multicast IPv4 (`IP_DROP_MEMBERSHIP`, `struct ip_mreq`).
1696pub fn leave_multicast_v4(sock: BorrowedFd<'_>, membership: &IpMembershipV4) -> Result<(), Errno> {
1697    set_ip_membership_v4(sock, IP_DROP_MEMBERSHIP, membership)
1698}
1699
1700/// **Rejoint** un groupe multicast IPv6 (`IPV6_ADD_MEMBERSHIP`, `struct ipv6_mreq`).
1701pub fn join_multicast_v6(sock: BorrowedFd<'_>, membership: &IpMembershipV6) -> Result<(), Errno> {
1702    set_ip_membership_v6(sock, IPV6_ADD_MEMBERSHIP, membership)
1703}
1704
1705/// **Quitte** un groupe multicast IPv6 (`IPV6_DROP_MEMBERSHIP`, `struct ipv6_mreq`).
1706pub fn leave_multicast_v6(sock: BorrowedFd<'_>, membership: &IpMembershipV6) -> Result<(), Errno> {
1707    set_ip_membership_v6(sock, IPV6_DROP_MEMBERSHIP, membership)
1708}
1709
1710// ─────────────────────────────────────────────────────────────────────────
1711// Helpers syscall bruts — x86_64
1712// ─────────────────────────────────────────────────────────────────────────
1713
1714#[cfg(target_arch = "x86_64")]
1715#[inline]
1716unsafe fn raw_syscall_socket(domain: i32, ty: i32, protocol: i32) -> i64 {
1717    let ret: i64;
1718    // SAFETY: SYS_socket (x86_64 = 41). Pas d'accès mémoire utilisateur.
1719    unsafe {
1720        core::arch::asm!(
1721            "syscall",
1722            in("rax") 41_i64,
1723            in("rdi") i64::from(domain),
1724            in("rsi") i64::from(ty),
1725            in("rdx") i64::from(protocol),
1726            lateout("rax") ret,
1727            lateout("rcx") _,
1728            lateout("r11") _,
1729            options(nostack, preserves_flags),
1730        );
1731    }
1732    ret
1733}
1734
1735#[cfg(target_arch = "x86_64")]
1736#[inline]
1737unsafe fn raw_syscall_bind(fd: i32, address: u64, addrlen: u32) -> i64 {
1738    let ret: i64;
1739    // SAFETY: SYS_bind (x86_64 = 49). Le kernel lit `addrlen` octets depuis `address`.
1740    unsafe {
1741        core::arch::asm!(
1742            "syscall",
1743            in("rax") 49_i64,
1744            in("rdi") i64::from(fd),
1745            in("rsi") address,
1746            in("rdx") i64::from(addrlen),
1747            lateout("rax") ret,
1748            lateout("rcx") _,
1749            lateout("r11") _,
1750            options(nostack, preserves_flags),
1751        );
1752    }
1753    ret
1754}
1755
1756#[cfg(target_arch = "x86_64")]
1757#[inline]
1758unsafe fn raw_syscall_listen(fd: i32, backlog: i32) -> i64 {
1759    let ret: i64;
1760    // SAFETY: SYS_listen (x86_64 = 50). Pas d'accès mémoire utilisateur.
1761    unsafe {
1762        core::arch::asm!(
1763            "syscall",
1764            in("rax") 50_i64,
1765            in("rdi") i64::from(fd),
1766            in("rsi") i64::from(backlog),
1767            lateout("rax") ret,
1768            lateout("rcx") _,
1769            lateout("r11") _,
1770            options(nostack, preserves_flags),
1771        );
1772    }
1773    ret
1774}
1775
1776#[cfg(target_arch = "x86_64")]
1777#[inline]
1778unsafe fn raw_syscall_connect(fd: i32, address: u64, addrlen: u32) -> i64 {
1779    let ret: i64;
1780    // SAFETY: SYS_connect (x86_64 = 42). Le kernel lit `addrlen` octets depuis `address`.
1781    unsafe {
1782        core::arch::asm!(
1783            "syscall",
1784            in("rax") 42_i64,
1785            in("rdi") i64::from(fd),
1786            in("rsi") address,
1787            in("rdx") i64::from(addrlen),
1788            lateout("rax") ret,
1789            lateout("rcx") _,
1790            lateout("r11") _,
1791            options(nostack, preserves_flags),
1792        );
1793    }
1794    ret
1795}
1796
1797#[cfg(target_arch = "x86_64")]
1798#[inline]
1799unsafe fn raw_syscall_accept4(fd: i32, address: u64, addrlen: u64, flags: i32) -> i64 {
1800    let ret: i64;
1801    // SAFETY: SYS_accept4 (x86_64 = 288). Le kernel écrit l'adresse du pair dans address.
1802    unsafe {
1803        core::arch::asm!(
1804            "syscall",
1805            in("rax") 288_i64,
1806            in("rdi") i64::from(fd),
1807            in("rsi") address,
1808            in("rdx") addrlen,
1809            in("r10") i64::from(flags),
1810            lateout("rax") ret,
1811            lateout("rcx") _,
1812            lateout("r11") _,
1813            options(nostack, preserves_flags),
1814        );
1815    }
1816    ret
1817}
1818
1819#[cfg(target_arch = "x86_64")]
1820#[inline]
1821unsafe fn raw_syscall_sendto(
1822    fd: i32,
1823    buffer: u64,
1824    length: usize,
1825    flags: u64,
1826    address: u64,
1827    addrlen: u32,
1828) -> i64 {
1829    let ret: i64;
1830    // SAFETY: SYS_sendto (x86_64 = 44). Le kernel lit `length` octets depuis `buffer`.
1831    unsafe {
1832        core::arch::asm!(
1833            "syscall",
1834            in("rax") 44_i64,
1835            in("rdi") i64::from(fd),
1836            in("rsi") buffer,
1837            in("rdx") length,
1838            in("r10") flags,
1839            in("r8") address,
1840            in("r9") i64::from(addrlen),
1841            lateout("rax") ret,
1842            lateout("rcx") _,
1843            lateout("r11") _,
1844            options(nostack, preserves_flags),
1845        );
1846    }
1847    ret
1848}
1849
1850#[cfg(target_arch = "x86_64")]
1851#[inline]
1852unsafe fn raw_syscall_recvfrom(
1853    fd: i32,
1854    buffer: u64,
1855    length: usize,
1856    flags: u64,
1857    address: u64,
1858    addrlen: u64,
1859) -> i64 {
1860    let ret: i64;
1861    // SAFETY: SYS_recvfrom (x86_64 = 45). Le kernel écrit au plus `length` octets dans `buffer`.
1862    unsafe {
1863        core::arch::asm!(
1864            "syscall",
1865            in("rax") 45_i64,
1866            in("rdi") i64::from(fd),
1867            in("rsi") buffer,
1868            in("rdx") length,
1869            in("r10") flags,
1870            in("r8") address,
1871            in("r9") addrlen,
1872            lateout("rax") ret,
1873            lateout("rcx") _,
1874            lateout("r11") _,
1875            options(nostack, preserves_flags),
1876        );
1877    }
1878    ret
1879}
1880
1881#[cfg(target_arch = "x86_64")]
1882#[inline]
1883unsafe fn raw_syscall_sendmsg(fd: i32, msghdr: u64, flags: u64) -> i64 {
1884    let ret: i64;
1885    // SAFETY: SYS_sendmsg (x86_64 = 46). Le kernel lit la msghdr et les buffers associés.
1886    unsafe {
1887        core::arch::asm!(
1888            "syscall",
1889            in("rax") 46_i64,
1890            in("rdi") i64::from(fd),
1891            in("rsi") msghdr,
1892            in("rdx") flags,
1893            lateout("rax") ret,
1894            lateout("rcx") _,
1895            lateout("r11") _,
1896            options(nostack, preserves_flags),
1897        );
1898    }
1899    ret
1900}
1901
1902#[cfg(target_arch = "x86_64")]
1903#[inline]
1904unsafe fn raw_syscall_recvmsg(fd: i32, msghdr: u64, flags: u64) -> i64 {
1905    let ret: i64;
1906    // SAFETY: SYS_recvmsg (x86_64 = 47). Le kernel écrit dans les buffers iov et cmsg.
1907    unsafe {
1908        core::arch::asm!(
1909            "syscall",
1910            in("rax") 47_i64,
1911            in("rdi") i64::from(fd),
1912            in("rsi") msghdr,
1913            in("rdx") flags,
1914            lateout("rax") ret,
1915            lateout("rcx") _,
1916            lateout("r11") _,
1917            options(nostack, preserves_flags),
1918        );
1919    }
1920    ret
1921}
1922
1923#[cfg(target_arch = "x86_64")]
1924#[inline]
1925unsafe fn raw_syscall_shutdown(fd: i32, how: i32) -> i64 {
1926    let ret: i64;
1927    // SAFETY: SYS_shutdown (x86_64 = 48). Pas d'accès mémoire utilisateur.
1928    unsafe {
1929        core::arch::asm!(
1930            "syscall",
1931            in("rax") 48_i64,
1932            in("rdi") i64::from(fd),
1933            in("rsi") i64::from(how),
1934            lateout("rax") ret,
1935            lateout("rcx") _,
1936            lateout("r11") _,
1937            options(nostack, preserves_flags),
1938        );
1939    }
1940    ret
1941}
1942
1943#[cfg(target_arch = "x86_64")]
1944#[inline]
1945unsafe fn raw_syscall_getsockname(fd: i32, address: u64, addrlen: u64) -> i64 {
1946    let ret: i64;
1947    // SAFETY: SYS_getsockname (x86_64 = 51). Le kernel écrit l'adresse et met à jour addrlen.
1948    unsafe {
1949        core::arch::asm!(
1950            "syscall",
1951            in("rax") 51_i64,
1952            in("rdi") i64::from(fd),
1953            in("rsi") address,
1954            in("rdx") addrlen,
1955            lateout("rax") ret,
1956            lateout("rcx") _,
1957            lateout("r11") _,
1958            options(nostack, preserves_flags),
1959        );
1960    }
1961    ret
1962}
1963
1964#[cfg(target_arch = "x86_64")]
1965#[inline]
1966unsafe fn raw_syscall_getpeername(fd: i32, address: u64, addrlen: u64) -> i64 {
1967    let ret: i64;
1968    // SAFETY: SYS_getpeername (x86_64 = 52). Le kernel écrit l'adresse et met à jour addrlen.
1969    unsafe {
1970        core::arch::asm!(
1971            "syscall",
1972            in("rax") 52_i64,
1973            in("rdi") i64::from(fd),
1974            in("rsi") address,
1975            in("rdx") addrlen,
1976            lateout("rax") ret,
1977            lateout("rcx") _,
1978            lateout("r11") _,
1979            options(nostack, preserves_flags),
1980        );
1981    }
1982    ret
1983}
1984
1985#[cfg(target_arch = "x86_64")]
1986#[inline]
1987unsafe fn raw_syscall_socketpair(domain: i32, ty: i32, protocol: i32, sv: u64) -> i64 {
1988    let ret: i64;
1989    // SAFETY: SYS_socketpair (x86_64 = 53). Le kernel écrit deux FDs dans sv[0..2].
1990    unsafe {
1991        core::arch::asm!(
1992            "syscall",
1993            in("rax") 53_i64,
1994            in("rdi") i64::from(domain),
1995            in("rsi") i64::from(ty),
1996            in("rdx") i64::from(protocol),
1997            in("r10") sv,
1998            lateout("rax") ret,
1999            lateout("rcx") _,
2000            lateout("r11") _,
2001            options(nostack, preserves_flags),
2002        );
2003    }
2004    ret
2005}
2006
2007#[cfg(target_arch = "x86_64")]
2008#[inline]
2009unsafe fn raw_syscall_getsockopt(
2010    fd: i32,
2011    level: i32,
2012    optname: i32,
2013    optval: u64,
2014    optlen: u64,
2015) -> i64 {
2016    let ret: i64;
2017    // SAFETY: SYS_getsockopt (x86_64 = 55). Le kernel écrit *optlen octets dans optval.
2018    unsafe {
2019        core::arch::asm!(
2020            "syscall",
2021            in("rax") 55_i64,
2022            in("rdi") i64::from(fd),
2023            in("rsi") i64::from(level),
2024            in("rdx") i64::from(optname),
2025            in("r10") optval,
2026            in("r8") optlen,
2027            lateout("rax") ret,
2028            lateout("rcx") _,
2029            lateout("r11") _,
2030            options(nostack, preserves_flags),
2031        );
2032    }
2033    ret
2034}
2035
2036#[cfg(target_arch = "x86_64")]
2037#[inline]
2038unsafe fn raw_syscall_setsockopt(
2039    fd: i32,
2040    level: i32,
2041    optname: i32,
2042    optval: u64,
2043    optlen: u32,
2044) -> i64 {
2045    let ret: i64;
2046    // SAFETY: SYS_setsockopt (x86_64 = 54). Le kernel lit optlen octets depuis optval.
2047    unsafe {
2048        core::arch::asm!(
2049            "syscall",
2050            in("rax") 54_i64,
2051            in("rdi") i64::from(fd),
2052            in("rsi") i64::from(level),
2053            in("rdx") i64::from(optname),
2054            in("r10") optval,
2055            in("r8") i64::from(optlen),
2056            lateout("rax") ret,
2057            lateout("rcx") _,
2058            lateout("r11") _,
2059            options(nostack, preserves_flags),
2060        );
2061    }
2062    ret
2063}
2064
2065// ─────────────────────────────────────────────────────────────────────────
2066// Helpers syscall bruts — aarch64
2067// ─────────────────────────────────────────────────────────────────────────
2068
2069#[cfg(target_arch = "aarch64")]
2070#[inline]
2071unsafe fn raw_syscall_socket(domain: i32, ty: i32, protocol: i32) -> i64 {
2072    let ret: i64;
2073    // SAFETY: SYS_socket (aarch64 = 198). Pas d'accès mémoire utilisateur.
2074    unsafe {
2075        core::arch::asm!(
2076            "svc 0",
2077            in("x8") 198_i64,
2078            inout("x0") i64::from(domain) => ret,
2079            in("x1") i64::from(ty),
2080            in("x2") i64::from(protocol),
2081            options(nostack, preserves_flags),
2082        );
2083    }
2084    ret
2085}
2086
2087#[cfg(target_arch = "aarch64")]
2088#[inline]
2089unsafe fn raw_syscall_bind(fd: i32, address: u64, addrlen: u32) -> i64 {
2090    let ret: i64;
2091    // SAFETY: SYS_bind (aarch64 = 200). Le kernel lit `addrlen` octets depuis `address`.
2092    unsafe {
2093        core::arch::asm!(
2094            "svc 0",
2095            in("x8") 200_i64,
2096            inout("x0") i64::from(fd) => ret,
2097            in("x1") address,
2098            in("x2") i64::from(addrlen),
2099            options(nostack, preserves_flags),
2100        );
2101    }
2102    ret
2103}
2104
2105#[cfg(target_arch = "aarch64")]
2106#[inline]
2107unsafe fn raw_syscall_listen(fd: i32, backlog: i32) -> i64 {
2108    let ret: i64;
2109    // SAFETY: SYS_listen (aarch64 = 201). Pas d'accès mémoire utilisateur.
2110    unsafe {
2111        core::arch::asm!(
2112            "svc 0",
2113            in("x8") 201_i64,
2114            inout("x0") i64::from(fd) => ret,
2115            in("x1") i64::from(backlog),
2116            options(nostack, preserves_flags),
2117        );
2118    }
2119    ret
2120}
2121
2122#[cfg(target_arch = "aarch64")]
2123#[inline]
2124unsafe fn raw_syscall_connect(fd: i32, address: u64, addrlen: u32) -> i64 {
2125    let ret: i64;
2126    // SAFETY: SYS_connect (aarch64 = 203). Le kernel lit `addrlen` octets depuis `address`.
2127    unsafe {
2128        core::arch::asm!(
2129            "svc 0",
2130            in("x8") 203_i64,
2131            inout("x0") i64::from(fd) => ret,
2132            in("x1") address,
2133            in("x2") i64::from(addrlen),
2134            options(nostack, preserves_flags),
2135        );
2136    }
2137    ret
2138}
2139
2140#[cfg(target_arch = "aarch64")]
2141#[inline]
2142unsafe fn raw_syscall_accept4(fd: i32, address: u64, addrlen: u64, flags: i32) -> i64 {
2143    let ret: i64;
2144    // SAFETY: SYS_accept4 (aarch64 = 242). Le kernel écrit l'adresse du pair dans address.
2145    unsafe {
2146        core::arch::asm!(
2147            "svc 0",
2148            in("x8") 242_i64,
2149            inout("x0") i64::from(fd) => ret,
2150            in("x1") address,
2151            in("x2") addrlen,
2152            in("x3") i64::from(flags),
2153            options(nostack, preserves_flags),
2154        );
2155    }
2156    ret
2157}
2158
2159#[cfg(target_arch = "aarch64")]
2160#[inline]
2161unsafe fn raw_syscall_sendto(
2162    fd: i32,
2163    buffer: u64,
2164    length: usize,
2165    flags: u64,
2166    address: u64,
2167    addrlen: u32,
2168) -> i64 {
2169    let ret: i64;
2170    // SAFETY: SYS_sendto (aarch64 = 206). Le kernel lit `length` octets depuis `buffer`.
2171    unsafe {
2172        core::arch::asm!(
2173            "svc 0",
2174            in("x8") 206_i64,
2175            inout("x0") i64::from(fd) => ret,
2176            in("x1") buffer,
2177            in("x2") length,
2178            in("x3") flags,
2179            in("x4") address,
2180            in("x5") i64::from(addrlen),
2181            options(nostack, preserves_flags),
2182        );
2183    }
2184    ret
2185}
2186
2187#[cfg(target_arch = "aarch64")]
2188#[inline]
2189unsafe fn raw_syscall_recvfrom(
2190    fd: i32,
2191    buffer: u64,
2192    length: usize,
2193    flags: u64,
2194    address: u64,
2195    addrlen: u64,
2196) -> i64 {
2197    let ret: i64;
2198    // SAFETY: SYS_recvfrom (aarch64 = 207). Le kernel écrit au plus `length` octets dans `buffer`.
2199    unsafe {
2200        core::arch::asm!(
2201            "svc 0",
2202            in("x8") 207_i64,
2203            inout("x0") i64::from(fd) => ret,
2204            in("x1") buffer,
2205            in("x2") length,
2206            in("x3") flags,
2207            in("x4") address,
2208            in("x5") addrlen,
2209            options(nostack, preserves_flags),
2210        );
2211    }
2212    ret
2213}
2214
2215#[cfg(target_arch = "aarch64")]
2216#[inline]
2217unsafe fn raw_syscall_sendmsg(fd: i32, msghdr: u64, flags: u64) -> i64 {
2218    let ret: i64;
2219    // SAFETY: SYS_sendmsg (aarch64 = 211). Le kernel lit la msghdr et les buffers associés.
2220    unsafe {
2221        core::arch::asm!(
2222            "svc 0",
2223            in("x8") 211_i64,
2224            inout("x0") i64::from(fd) => ret,
2225            in("x1") msghdr,
2226            in("x2") flags,
2227            options(nostack, preserves_flags),
2228        );
2229    }
2230    ret
2231}
2232
2233#[cfg(target_arch = "aarch64")]
2234#[inline]
2235unsafe fn raw_syscall_recvmsg(fd: i32, msghdr: u64, flags: u64) -> i64 {
2236    let ret: i64;
2237    // SAFETY: SYS_recvmsg (aarch64 = 212). Le kernel écrit dans les buffers iov et cmsg.
2238    unsafe {
2239        core::arch::asm!(
2240            "svc 0",
2241            in("x8") 212_i64,
2242            inout("x0") i64::from(fd) => ret,
2243            in("x1") msghdr,
2244            in("x2") flags,
2245            options(nostack, preserves_flags),
2246        );
2247    }
2248    ret
2249}
2250
2251#[cfg(target_arch = "aarch64")]
2252#[inline]
2253unsafe fn raw_syscall_shutdown(fd: i32, how: i32) -> i64 {
2254    let ret: i64;
2255    // SAFETY: SYS_shutdown (aarch64 = 210). Pas d'accès mémoire utilisateur.
2256    unsafe {
2257        core::arch::asm!(
2258            "svc 0",
2259            in("x8") 210_i64,
2260            inout("x0") i64::from(fd) => ret,
2261            in("x1") i64::from(how),
2262            options(nostack, preserves_flags),
2263        );
2264    }
2265    ret
2266}
2267
2268#[cfg(target_arch = "aarch64")]
2269#[inline]
2270unsafe fn raw_syscall_getsockname(fd: i32, address: u64, addrlen: u64) -> i64 {
2271    let ret: i64;
2272    // SAFETY: SYS_getsockname (aarch64 = 204). Le kernel écrit l'adresse et met à jour addrlen.
2273    unsafe {
2274        core::arch::asm!(
2275            "svc 0",
2276            in("x8") 204_i64,
2277            inout("x0") i64::from(fd) => ret,
2278            in("x1") address,
2279            in("x2") addrlen,
2280            options(nostack, preserves_flags),
2281        );
2282    }
2283    ret
2284}
2285
2286#[cfg(target_arch = "aarch64")]
2287#[inline]
2288unsafe fn raw_syscall_getpeername(fd: i32, address: u64, addrlen: u64) -> i64 {
2289    let ret: i64;
2290    // SAFETY: SYS_getpeername (aarch64 = 205). Le kernel écrit l'adresse et met à jour addrlen.
2291    unsafe {
2292        core::arch::asm!(
2293            "svc 0",
2294            in("x8") 205_i64,
2295            inout("x0") i64::from(fd) => ret,
2296            in("x1") address,
2297            in("x2") addrlen,
2298            options(nostack, preserves_flags),
2299        );
2300    }
2301    ret
2302}
2303
2304#[cfg(target_arch = "aarch64")]
2305#[inline]
2306unsafe fn raw_syscall_socketpair(domain: i32, ty: i32, protocol: i32, sv: u64) -> i64 {
2307    let ret: i64;
2308    // SAFETY: SYS_socketpair (aarch64 = 199). Le kernel écrit deux FDs dans sv[0..2].
2309    unsafe {
2310        core::arch::asm!(
2311            "svc 0",
2312            in("x8") 199_i64,
2313            inout("x0") i64::from(domain) => ret,
2314            in("x1") i64::from(ty),
2315            in("x2") i64::from(protocol),
2316            in("x3") sv,
2317            options(nostack, preserves_flags),
2318        );
2319    }
2320    ret
2321}
2322
2323#[cfg(target_arch = "aarch64")]
2324#[inline]
2325unsafe fn raw_syscall_getsockopt(
2326    fd: i32,
2327    level: i32,
2328    optname: i32,
2329    optval: u64,
2330    optlen: u64,
2331) -> i64 {
2332    let ret: i64;
2333    // SAFETY: SYS_getsockopt (aarch64 = 209). Le kernel écrit *optlen octets dans optval.
2334    unsafe {
2335        core::arch::asm!(
2336            "svc 0",
2337            in("x8") 209_i64,
2338            inout("x0") i64::from(fd) => ret,
2339            in("x1") i64::from(level),
2340            in("x2") i64::from(optname),
2341            in("x3") optval,
2342            in("x4") optlen,
2343            options(nostack, preserves_flags),
2344        );
2345    }
2346    ret
2347}
2348
2349#[cfg(target_arch = "aarch64")]
2350#[inline]
2351unsafe fn raw_syscall_setsockopt(
2352    fd: i32,
2353    level: i32,
2354    optname: i32,
2355    optval: u64,
2356    optlen: u32,
2357) -> i64 {
2358    let ret: i64;
2359    // SAFETY: SYS_setsockopt (aarch64 = 208). Le kernel lit optlen octets depuis optval.
2360    unsafe {
2361        core::arch::asm!(
2362            "svc 0",
2363            in("x8") 208_i64,
2364            inout("x0") i64::from(fd) => ret,
2365            in("x1") i64::from(level),
2366            in("x2") i64::from(optname),
2367            in("x3") optval,
2368            in("x4") i64::from(optlen),
2369            options(nostack, preserves_flags),
2370        );
2371    }
2372    ret
2373}
2374
2375// ─────────────────────────────────────────────────────────────────────────
2376// Tests
2377// ─────────────────────────────────────────────────────────────────────────
2378
2379#[cfg(all(test, target_os = "linux"))]
2380mod tests;