aya-friday 0.13.2

An eBPF library with a focus on developer experience and operability.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
//! Probes and identifies available eBPF features supported by the host kernel.

use std::{
    ffi::CStr,
    mem,
    os::fd::{AsFd as _, AsRawFd as _},
    ptr,
};

use aya_obj::{
    btf::{Btf, BtfKind},
    generated::{
        BPF_CALL, BPF_EXIT, BPF_F_MMAPABLE, BPF_F_NO_PREALLOC, BPF_JMP, bpf_attr, bpf_cmd,
        bpf_func_id, bpf_map_type, bpf_prog_info,
    },
};
use libc::{E2BIG, EBADF, EINVAL};

use super::{
    SyscallError, bpf_map_create, bpf_prog_load, bpf_raw_tracepoint_open, new_insn, unit_sys_bpf,
    with_prog_insns, with_trivial_prog,
};
use crate::{
    MockableFd,
    maps::MapType,
    programs::{LsmAttachType, ProgramError, ProgramType},
    util::page_size,
};

/// A BPF helper function.
#[doc(alias = "bpf_func_id")]
pub type BpfHelper = bpf_func_id;

/// Whether the host kernel supports the [`BpfHelper`] for the [`ProgramType`].
///
/// Helper availability is program-type specific. This probes whether the
/// `(program_type, helper)` pair is supported by the current environment.
/// `Ok(false)` means the pair is unavailable; it does not distinguish whether
/// the helper is absent entirely, unsupported for the program type, or disabled
/// by the current kernel configuration.
/// This follows libbpf's probe strategy by loading a minimal program that calls
/// the requested helper and inspecting verifier output for unknown-helper
/// diagnostics.
///
/// # Examples
///
/// ```no_run
/// # use aya::{programs::ProgramType, sys::{BpfHelper, is_helper_supported}};
/// #
/// match is_helper_supported(ProgramType::Xdp, BpfHelper::BPF_FUNC_redirect) {
///     Ok(true) => println!("bpf_redirect supported from XDP"),
///     Ok(false) => println!("bpf_redirect not supported from XDP"),
///     Err(err) => println!("unexpected error while probing: {:?}", err),
/// }
/// ```
///
/// # Errors
///
/// Returns [`ProgramError::SyscallError`] if probing fails before support can
/// be determined.
/// Returns [`ProgramError::UnexpectedProgramType`] for [`ProgramType::Tracing`],
/// [`ProgramType::Extension`], [`ProgramType::Lsm`], and
/// [`ProgramType::StructOps`], which require a real attach or BTF target.
pub fn is_helper_supported(
    program_type: ProgramType,
    helper: BpfHelper,
) -> Result<bool, ProgramError> {
    if program_type == ProgramType::Unspecified {
        return Ok(false);
    }

    // These program types require a real attach or BTF target, so a minimal
    // helper-call program cannot probe their helper availability reliably.
    // https://github.com/libbpf/libbpf/blob/v1.7.0/src/libbpf_probes.c#L434-L442
    if matches!(
        program_type,
        ProgramType::Tracing
            | ProgramType::Extension
            | ProgramType::Lsm(_)
            | ProgramType::StructOps
    ) {
        return Err(ProgramError::UnexpectedProgramType);
    }

    let call = (BPF_JMP | BPF_CALL) as u8;
    let exit = (BPF_JMP | BPF_EXIT) as u8;
    let insns = [
        new_insn(call, 0, 0, 0, helper as i32),
        new_insn(exit, 0, 0, 0, 0),
    ];
    // 4096 bytes is enough for this probe and matches libbpf's helper probe.
    // https://github.com/libbpf/libbpf/blob/v1.7.0/src/libbpf_probes.c#L430-L479
    let mut verifier_log = [0u8; 4096];

    with_prog_insns(program_type, &insns, |attr| {
        // SAFETY: union access
        let u = unsafe { &mut attr.__bindgen_anon_3 };
        u.log_buf = verifier_log.as_mut_ptr() as u64;
        u.log_level = 1;
        u.log_size = verifier_log.len() as u32;
        match bpf_prog_load(attr).map(|_: MockableFd| ()) {
            Ok(()) => Ok(true),
            Err(io_error) => {
                // https://github.com/libbpf/libbpf/blob/v1.7.0/src/libbpf_probes.c#L452-L466
                const UNSUPPORTED_HELPER_DIAGNOSTICS: &[&[u8]] = &[
                    b"invalid func ",
                    b"unknown func ",
                    b"program of this type cannot use helper ",
                ];
                let verifier_log = CStr::from_bytes_until_nul(&verifier_log)
                    .map_or(verifier_log.as_slice(), CStr::to_bytes);

                if verifier_log.is_empty() {
                    return match io_error.raw_os_error() {
                        // With a valid probe attr, EINVAL means the program type is unsupported.
                        Some(EINVAL) => Ok(false),
                        // An empty verifier log means the load failed before reaching the
                        // verifier. In this path, `E2BIG` from `bpf_check_uarg_tail_zero()`
                        // indicates that the kernel detected non-zero `bpf_attr` fields it does
                        // not know about before verifier output could be produced.
                        Some(E2BIG) => Ok(false),
                        _ => Err(ProgramError::SyscallError(SyscallError {
                            call: "bpf_prog_load",
                            io_error,
                        })),
                    };
                }

                if UNSUPPORTED_HELPER_DIAGNOSTICS.iter().any(|diagnostic| {
                    verifier_log
                        .windows(diagnostic.len())
                        .any(|w| w == *diagnostic)
                }) {
                    return Ok(false);
                }

                // Other verifier failures mean the helper was recognized; assume support.
                Ok(true)
            }
        }
    })
}

/// Whether the host kernel supports the [`ProgramType`].
///
/// # Examples
///
/// ```no_run
/// # use aya::{programs::ProgramType, sys::is_program_supported};
/// #
/// match is_program_supported(ProgramType::Xdp) {
///     Ok(true) => println!("XDP supported :)"),
///     Ok(false) => println!("XDP not supported :("),
///     Err(err) => println!("unexpected error while probing: {:?}", err),
/// }
/// ```
///
/// # Errors
///
/// Returns [`ProgramError::SyscallError`] if a syscall fails with an unexpected
/// error, or [`ProgramError::Btf`] for BTF related errors.
///
/// Certain errors are expected and handled internally; only unanticipated
/// failures during probing will result in these errors.
pub fn is_program_supported(program_type: ProgramType) -> Result<bool, ProgramError> {
    if program_type == ProgramType::Unspecified {
        return Ok(false);
    }

    // Verifier log is used in tracing, extension, and lsm to detect support if loading fails due
    // to unset `attach_btf_id`. A valid `attach_btf_id` is required for a successful load, but is
    // left unset if the hook functions cannot be found in the BTF.
    //
    // If the program types are supported, but the field is unset, then the following message[0]
    // is emitted to the verifier log:
    // `Tracing programs must provide btf_id\nprocessed 0 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0\n\0`
    //
    // Otherwise, if the program types are not supported, then the verifier log will be empty.
    //
    // [0] https://elixir.bootlin.com/linux/v5.5/source/kernel/bpf/verifier.c#L9535
    let mut verifier_log = matches!(
        program_type,
        ProgramType::Tracing | ProgramType::Extension | ProgramType::Lsm(_)
    )
    .then_some([0u8; 256]);

    // Both tracing and lsm types require a valid `attach_btf_id` to load successfully. However, if
    // the symbols cannot be found in the BTF, then leave the field unset/0.
    //
    // The extension type also requires an `attach_btf_id`, but we intentionally leave it unset
    // since a successful load requires additional setup with a separate BTF-backed program.
    //
    // When `attach_btf_id` is unset, then loading will fail and so we examine verifier log for the
    // expected message.
    let attach_btf_id = match program_type {
        // `bpf_fentry_test1` symbol from:
        // https://elixir.bootlin.com/linux/v5.5/source/net/bpf/test_run.c#L112
        ProgramType::Tracing => Some("bpf_fentry_test1"),
        // `bpf_lsm_bpf` symbol from:
        // - https://elixir.bootlin.com/linux/v5.7/source/include/linux/lsm_hook_defs.h#L364
        // - or https://elixir.bootlin.com/linux/v5.11/source/kernel/bpf/bpf_lsm.c#L135 on later versions
        ProgramType::Lsm(_) => Some("bpf_lsm_bpf"),
        _ => None,
    }
    .map(|func_name| {
        Btf::from_sys_fs()
            .and_then(|btf| btf.id_by_type_name_kind(func_name, BtfKind::Func))
            .unwrap_or(0)
    });

    with_trivial_prog(program_type, |attr| {
        // SAFETY: union access
        let u = unsafe { &mut attr.__bindgen_anon_3 };

        if let Some(attach_btf_id) = attach_btf_id {
            u.attach_btf_id = attach_btf_id;
        }
        // If loading fails for tracing, extension, and lsm types due to unset `attach_btf_id`,
        // then we defer to verifier log to verify whether type is supported.
        if let Some(verifier_log) = verifier_log.as_mut() {
            u.log_buf = verifier_log.as_mut_ptr() as u64;
            u.log_level = 1;
            u.log_size = verifier_log.len() as u32;
        }

        match bpf_prog_load(attr) {
            Err(io_error) => match io_error.raw_os_error() {
                // Loading may fail for some types (namely tracing, extension, lsm, & struct_ops), so we
                // perform additional examination on the OS error and/or verifier logs.
                //
                // For most types, `EINVAL` typically indicates it is not supported.
                // However, further examination is required for tracing, extension, and lsm.
                Some(EINVAL) => {
                    // At this point for tracing, extension, and lsm, loading failed due to unset
                    // `attach_btf_id`, so we examine verifier log for the target message. The
                    // message originated from `check_attach_btf_id()`[0] in v5.5 to v5.9, then
                    // moved to `bpf_check_attach_target()`[1] in v5.10 and onward.
                    //
                    // If target message is present in the logs, then loading process has reached
                    // up to the verifier section, which indicates that the kernel is at least
                    // aware of the program type variants.
                    //
                    // If the verifier log is empty, then it was immediately rejected by the
                    // kernel, meaning the types are not supported.
                    //
                    // [0] https://elixir.bootlin.com/linux/v5.5/source/kernel/bpf/verifier.c#L9535
                    // [1] https://elixir.bootlin.com/linux/v5.9/source/kernel/bpf/verifier.c#L10849
                    let supported = matches!(
                        verifier_log,
                        Some(verifier_log) if verifier_log.starts_with(b"Tracing programs must provide btf_id")
                    );
                    Ok(supported)
                }
                // `E2BIG` from `bpf_check_uarg_tail_zero()`[0] indicates that the kernel detected
                // non-zero fields in `bpf_attr` that does not exist at its current version.
                //
                // [0] https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/syscall.c#L71
                Some(E2BIG) => Ok(false),
                // `ENOTSUPP` from `check_struct_ops_btf_id()`[0] indicates that it reached the
                // verifier section, meaning the kernel is at least aware of the type's existence.
                //
                // Otherwise, it will produce `EINVAL`, meaning the type is immediately rejected
                // and does not exist.
                //
                // [0] https://elixir.bootlin.com/linux/v5.6/source/kernel/bpf/verifier.c#L9740
                Some(524) if program_type == ProgramType::StructOps => Ok(true),
                _ => Err(ProgramError::SyscallError(SyscallError {
                    call: "bpf_prog_load",
                    io_error,
                })),
            },
            Ok(prog_fd) => {
                // Some kernels can load tracing and LSM programs but cannot attach BPF
                // trampolines: `bpf_raw_tracepoint_open` routes them to
                // `bpf_tracing_prog_attach()`, and trampoline registration can fail with
                // `-ENOTSUPP`. Probe attach support explicitly. This is notably seen on arm64
                // kernels before 6.4.
                //
                // https://github.com/torvalds/linux/blob/v6.3/kernel/bpf/syscall.c#L3319-L3333
                // https://github.com/torvalds/linux/blob/v6.3/kernel/bpf/trampoline.c#L234-L237
                //
                // h/t to https://www.exein.io/blog/exploring-bpf-lsm-support-on-aarch64-with-ftrace.
                //
                // The same test for cGroup LSM programs would require attaching to a real cgroup,
                // which is more involved and not possible in the general case.
                if matches!(
                    program_type,
                    ProgramType::Tracing | ProgramType::Lsm(LsmAttachType::Mac)
                ) {
                    match bpf_raw_tracepoint_open(None, prog_fd.as_fd()) {
                        Ok(_) => Ok(true),
                        Err(io_error) => match io_error.raw_os_error() {
                            Some(524) => Ok(false),
                            _ => Err(ProgramError::SyscallError(SyscallError {
                                call: "bpf_raw_tracepoint_open",
                                io_error,
                            })),
                        },
                    }
                } else {
                    Ok(true)
                }
            }
        }
    })
}

/// Whether the host kernel supports the [`MapType`].
///
/// # Examples
///
/// ```no_run
/// # use aya::{maps::MapType, sys::is_map_supported};
/// #
/// match is_map_supported(MapType::HashOfMaps) {
///     Ok(true) => println!("hash_of_maps supported :)"),
///     Ok(false) => println!("hash_of_maps not supported :("),
///     Err(err) => println!("unexpected error while probing: {:?}", err),
/// }
/// ```
///
/// # Errors
///
/// Returns [`SyscallError`] if kernel probing fails with an unexpected error.
///
/// Note that certain errors are expected and handled internally; only
/// unanticipated failures during probing will result in this error.
pub fn is_map_supported(map_type: MapType) -> Result<bool, SyscallError> {
    // Each `bpf_map_ops` struct contains their own `.map_alloc()` & `.map_alloc_check()` that does
    // field validation on map_create.
    let (key_size, value_size, max_entries) = match map_type {
        MapType::Unspecified => return Ok(false),
        MapType::Hash                   // https://elixir.bootlin.com/linux/v3.19/source/kernel/bpf/hashtab.c#L349
        | MapType::PerCpuHash           // https://elixir.bootlin.com/linux/v4.6/source/kernel/bpf/hashtab.c#L726
        | MapType::LruHash              // https://elixir.bootlin.com/linux/v4.10/source/kernel/bpf/hashtab.c#L1032
        | MapType::LruPerCpuHash        // https://elixir.bootlin.com/linux/v4.10/source/kernel/bpf/hashtab.c#L1133
            => (1, 1, 1),
        MapType::Array                  // https://elixir.bootlin.com/linux/v3.19/source/kernel/bpf/arraymap.c#L138
        | MapType::PerCpuArray          // https://elixir.bootlin.com/linux/v4.6/source/kernel/bpf/arraymap.c#L283
            => (4, 1, 1),
        MapType::ProgramArray           // https://elixir.bootlin.com/linux/v4.2/source/kernel/bpf/arraymap.c#L239
        | MapType::PerfEventArray       // https://elixir.bootlin.com/linux/v4.3/source/kernel/bpf/arraymap.c#L312
        | MapType::CgroupArray          // https://elixir.bootlin.com/linux/v4.8/source/kernel/bpf/arraymap.c#L562
        | MapType::ArrayOfMaps          // https://elixir.bootlin.com/linux/v4.12/source/kernel/bpf/arraymap.c#L595
        | MapType::DevMap               // https://elixir.bootlin.com/linux/v4.14/source/kernel/bpf/devmap.c#L360
        | MapType::SockMap              // https://elixir.bootlin.com/linux/v4.14/source/kernel/bpf/sockmap.c#L874
        | MapType::CpuMap               // https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/cpumap.c#L589
        | MapType::XskMap               // https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/xskmap.c#L224
        | MapType::ReuseportSockArray   // https://elixir.bootlin.com/linux/v4.20/source/kernel/bpf/reuseport_array.c#L357
        | MapType::DevMapHash           // https://elixir.bootlin.com/linux/v5.4/source/kernel/bpf/devmap.c#L713
            => (4, 4, 1),
        MapType::StackTrace             // https://elixir.bootlin.com/linux/v4.6/source/kernel/bpf/stackmap.c#L272
            => (4, 8, 1),
        MapType::LpmTrie                // https://elixir.bootlin.com/linux/v4.11/source/kernel/bpf/lpm_trie.c#L509
            => (8, 1, 1),
        MapType::HashOfMaps             // https://elixir.bootlin.com/linux/v4.12/source/kernel/bpf/hashtab.c#L1301
        | MapType::SockHash             // https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/sockmap.c#L2507
            => (1, 4, 1),
        MapType::CgroupStorage          // https://elixir.bootlin.com/linux/v4.19/source/kernel/bpf/local_storage.c#L246
        | MapType::PerCpuCgroupStorage  // https://elixir.bootlin.com/linux/v4.20/source/kernel/bpf/local_storage.c#L313
            => (16, 1, 0),
        MapType::Queue                  // https://elixir.bootlin.com/linux/v4.20/source/kernel/bpf/queue_stack_maps.c#L267
        | MapType::Stack                // https://elixir.bootlin.com/linux/v4.20/source/kernel/bpf/queue_stack_maps.c#L280
        | MapType::BloomFilter          // https://elixir.bootlin.com/linux/v5.16/source/kernel/bpf/bloom_filter.c#L193
            => (0, 1, 1),
        MapType::SkStorage              // https://elixir.bootlin.com/linux/v5.2/source/net/core/bpf_sk_storage.c#L779
        | MapType::InodeStorage         // https://elixir.bootlin.com/linux/v5.10/source/kernel/bpf/bpf_inode_storage.c#L239
        | MapType::TaskStorage          // https://elixir.bootlin.com/linux/v5.11/source/kernel/bpf/bpf_task_storage.c#L285
        | MapType::CgrpStorage          // https://elixir.bootlin.com/linux/v6.2/source/kernel/bpf/bpf_cgrp_storage.c#L216
            => (4, 1, 0),
        MapType::StructOps              // https://elixir.bootlin.com/linux/v5.6/source/kernel/bpf/bpf_struct_ops.c#L607
            => (4, 0, 1),
        MapType::RingBuf                // https://elixir.bootlin.com/linux/v5.8/source/kernel/bpf/ringbuf.c#L296
        | MapType::UserRingBuf          // https://elixir.bootlin.com/linux/v6.1/source/kernel/bpf/ringbuf.c#L356
        // `max_entries` is required to be multiple of kernel page size & power of 2:
        // https://elixir.bootlin.com/linux/v5.8/source/kernel/bpf/ringbuf.c#L160
            => (0, 0, page_size() as u32),
        MapType::Arena                  // https://elixir.bootlin.com/linux/v6.9/source/kernel/bpf/arena.c#L380
            => (0, 0, 1),
    };

    // SAFETY: all-zero byte-pattern valid for `bpf_attr`
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
    // SAFETY: union access
    let u = unsafe { &mut attr.__bindgen_anon_1 };
    u.map_type = map_type as u32;
    u.key_size = key_size;
    u.value_size = value_size;
    u.max_entries = max_entries;

    // Ensure that fd doesn't get dropped due to scoping for *_of_maps type.
    let inner_map_fd: MockableFd;
    match map_type {
        // lpm_trie is required to not be pre-alloced[0].
        //
        // https://elixir.bootlin.com/linux/v4.11/source/kernel/bpf/lpm_trie.c#L419
        MapType::LpmTrie => u.map_flags = BPF_F_NO_PREALLOC,
        // For these types, we aim to intentionally trigger `EBADF` by supplying invalid btf attach
        // data to verify the map type's existence. Otherwise, negative support will produce
        // `EINVAL` instead.
        MapType::SkStorage
        | MapType::InodeStorage
        | MapType::TaskStorage
        | MapType::CgrpStorage => {
            // These types are required to not be pre-alloced:
            // - sk_storage: https://elixir.bootlin.com/linux/v5.2/source/net/core/bpf_sk_storage.c#L604
            // - inode_storage: https://elixir.bootlin.com/linux/v5.10/source/kernel/bpf/bpf_local_storage.c#L525
            // - task_storage: https://elixir.bootlin.com/linux/v5.11/source/kernel/bpf/bpf_local_storage.c#L527
            // - cgrp_storage: https://elixir.bootlin.com/linux/v6.2/source/kernel/bpf/bpf_local_storage.c#L539
            u.map_flags = BPF_F_NO_PREALLOC;
            // Intentionally trigger `EBADF` from `btf_get_by_fd()`[0].
            //
            // [0] https://elixir.bootlin.com/linux/v5.2/source/kernel/bpf/btf.c#L3428
            u.btf_fd = u32::MAX;
            u.btf_key_type_id = 1;
            u.btf_value_type_id = 1;
        }
        MapType::ArrayOfMaps | MapType::HashOfMaps => {
            // SAFETY: all-zero byte-pattern valid for `bpf_attr`
            let mut attr_map = unsafe { mem::zeroed::<bpf_attr>() };
            // SAFETY: union access
            let u_map = unsafe { &mut attr_map.__bindgen_anon_1 };
            u_map.map_type = bpf_map_type::BPF_MAP_TYPE_HASH as u32;
            u_map.key_size = 1;
            u_map.value_size = 1;
            u_map.max_entries = 1;
            inner_map_fd = bpf_map_create(&mut attr_map).map_err(|io_error| SyscallError {
                call: "bpf_map_create",
                io_error,
            })?;

            u.inner_map_fd = inner_map_fd.as_raw_fd() as u32;
        }
        // We aim to intentionally trigger `ENOTSUPP` by setting an invalid, non-zero
        // `btf_vmlinux_value_type_id`. Negative support produce `EINVAL` instead.
        MapType::StructOps => u.btf_vmlinux_value_type_id = 1,
        // arena is required to be mmapable[0].
        //
        // [0] https://elixir.bootlin.com/linux/v6.9/source/kernel/bpf/arena.c#L103
        MapType::Arena => u.map_flags = BPF_F_MMAPABLE,
        _ => {}
    }

    // BPF_MAP_CREATE returns a new file descriptor.
    let io_error = match bpf_map_create(&mut attr) {
        Ok(_fd) => return Ok(true),
        Err(io_error) => io_error,
    };

    // sk_storage, struct_ops, inode_storage, task_storage, & cgrp_storage requires further
    // examination to verify support.
    match io_error.raw_os_error() {
        Some(EINVAL) => Ok(false),
        // These types use fields that may not exist at the kernel's current version. Supplying
        // `bpf_attr` fields unknown to the kernel triggers `E2BIG` from `bpf_check_uarg_tail_zero()`[0].
        //
        // [0] https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/syscall.c#L71
        Some(E2BIG)
            if matches!(
                map_type,
                MapType::SkStorage
                    | MapType::StructOps
                    | MapType::InodeStorage
                    | MapType::TaskStorage
                    | MapType::CgrpStorage
            ) =>
        {
            Ok(false)
        }
        // For these types, `EBADF` from `btf_get_by_fd()`[0] indicates that map_create advanced
        // far enough in the validation to recognize the type before being rejected.
        //
        // Otherwise, negative support produces `EINVAL`, meaning it was immediately rejected.
        //
        // [0] https://elixir.bootlin.com/linux/v5.2/source/kernel/bpf/btf.c#L3428
        Some(EBADF)
            if matches!(
                map_type,
                MapType::SkStorage
                    | MapType::InodeStorage
                    | MapType::TaskStorage
                    | MapType::CgrpStorage
            ) =>
        {
            Ok(true)
        }
        // `ENOTSUPP` from `bpf_struct_ops_map_alloc()`[0] indicates that map_create advanced far
        // enough in the validation to recognize the type before being rejected.
        //
        // Otherwise, negative support produces `EINVAL`, meaning it was immediately rejected.
        //
        // [0] https://elixir.bootlin.com/linux/v5.6/source/kernel/bpf/bpf_struct_ops.c#L557
        Some(524) if map_type == MapType::StructOps => Ok(true),
        _ => Err(SyscallError {
            call: "bpf_map_create",
            io_error,
        }),
    }
}

/// Whether `nr_map_ids` & `map_ids` fields in `bpf_prog_info` are supported.
pub(crate) fn is_prog_info_map_ids_supported() -> Result<bool, ProgramError> {
    let fd = with_trivial_prog(ProgramType::SocketFilter, |attr| {
        bpf_prog_load(attr).map_err(|io_error| {
            ProgramError::SyscallError(SyscallError {
                call: "bpf_prog_load",
                io_error,
            })
        })
    })?;
    // SAFETY: all-zero byte-pattern valid for `bpf_prog_info`
    let mut info = unsafe { mem::zeroed::<bpf_prog_info>() };
    info.nr_map_ids = 1;

    probe_bpf_info(fd, info).map_err(ProgramError::from)
}

/// Tests whether `bpf_prog_info.gpl_compatible` field is supported.
pub(crate) fn is_prog_info_license_supported() -> Result<bool, ProgramError> {
    let fd = with_trivial_prog(ProgramType::SocketFilter, |attr| {
        bpf_prog_load(attr).map_err(|io_error| {
            ProgramError::SyscallError(SyscallError {
                call: "bpf_prog_load",
                io_error,
            })
        })
    })?;
    // SAFETY: all-zero byte-pattern valid for `bpf_prog_info`
    let mut info = unsafe { mem::zeroed::<bpf_prog_info>() };
    info.set_gpl_compatible(1);

    probe_bpf_info(fd, info).map_err(ProgramError::from)
}

/// Probes program and map info.
fn probe_bpf_info<T>(fd: MockableFd, info: T) -> Result<bool, SyscallError> {
    // SAFETY: all-zero byte-pattern valid for `bpf_attr`
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
    attr.info.bpf_fd = fd.as_raw_fd() as u32;
    attr.info.info_len = size_of_val(&info) as u32;
    attr.info.info = ptr::from_ref(&info) as u64;

    let io_error = match unit_sys_bpf(bpf_cmd::BPF_OBJ_GET_INFO_BY_FD, &mut attr) {
        Ok(()) => return Ok(true),
        Err(io_error) => io_error,
    };
    match io_error.raw_os_error() {
        // `E2BIG` from `bpf_check_uarg_tail_zero()`
        Some(E2BIG) => Ok(false),
        _ => Err(SyscallError {
            call: "bpf_obj_get_info_by_fd",
            io_error,
        }),
    }
}