Skip to main content

scx_utils/
compat.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2//
3// This software may be used and distributed according to the terms of the
4// GNU General Public License version 2.
5
6use anyhow::{Context, Result, anyhow, bail};
7use libbpf_rs::libbpf_sys::*;
8use libbpf_rs::{AsRawLibbpf, OpenObject, OpenProgramImpl, ProgramImpl};
9use log::{debug, error, info, warn};
10use std::env;
11use std::ffi::CStr;
12use std::ffi::CString;
13use std::ffi::OsStr;
14use std::ffi::c_void;
15use std::io;
16use std::io::BufRead;
17use std::io::BufReader;
18use std::mem::size_of;
19use std::os::fd::AsFd;
20use std::os::fd::AsRawFd;
21use std::os::fd::BorrowedFd;
22use std::os::fd::FromRawFd;
23use std::os::fd::OwnedFd;
24use std::slice::from_raw_parts;
25
26const PROCFS_MOUNTS: &str = "/proc/mounts";
27const TRACEFS: &str = "tracefs";
28const DEBUGFS: &str = "debugfs";
29
30mod enums_abi {
31    include!("enums_abi.autogen.rs");
32}
33
34lazy_static::lazy_static! {
35    pub static ref SCX_OPS_KEEP_BUILTIN_IDLE: u64 =
36        read_enum("scx_ops_flags", "SCX_OPS_KEEP_BUILTIN_IDLE").unwrap_or(0);
37    pub static ref SCX_OPS_ENQ_LAST: u64 =
38        read_enum("scx_ops_flags", "SCX_OPS_ENQ_LAST").unwrap_or(0);
39    pub static ref SCX_OPS_ENQ_EXITING: u64 =
40        read_enum("scx_ops_flags", "SCX_OPS_ENQ_EXITING").unwrap_or(0);
41    pub static ref SCX_OPS_SWITCH_PARTIAL: u64 =
42        read_enum("scx_ops_flags", "SCX_OPS_SWITCH_PARTIAL").unwrap_or(0);
43    pub static ref SCX_OPS_ENQ_MIGRATION_DISABLED: u64 =
44        read_enum("scx_ops_flags", "SCX_OPS_ENQ_MIGRATION_DISABLED").unwrap_or(0);
45    pub static ref SCX_OPS_ALLOW_QUEUED_WAKEUP: u64 =
46        read_enum("scx_ops_flags", "SCX_OPS_ALLOW_QUEUED_WAKEUP").unwrap_or(0);
47    pub static ref SCX_OPS_BUILTIN_IDLE_PER_NODE: u64 =
48        read_enum("scx_ops_flags", "SCX_OPS_BUILTIN_IDLE_PER_NODE").unwrap_or(0);
49    pub static ref SCX_OPS_ALWAYS_ENQ_IMMED: u64 =
50        read_enum("scx_ops_flags", "SCX_OPS_ALWAYS_ENQ_IMMED").unwrap_or(0);
51    pub static ref SCX_OPS_TID_TO_TASK: u64 =
52        read_enum("scx_ops_flags", "SCX_OPS_TID_TO_TASK").unwrap_or(0);
53    pub static ref SCX_OPS_ENQ_BLOCKED: u64 =
54        read_enum("scx_ops_flags", "SCX_OPS_ENQ_BLOCKED").unwrap_or(0);
55
56    pub static ref SCX_PICK_IDLE_CORE: u64 =
57        read_enum("scx_pick_idle_cpu_flags", "SCX_PICK_IDLE_CORE").unwrap_or(0);
58    pub static ref SCX_PICK_IDLE_IN_NODE: u64 =
59        read_enum("scx_pick_idle_cpu_flags", "SCX_PICK_IDLE_IN_NODE").unwrap_or(0);
60
61    pub static ref ROOT_PREFIX: String =
62        env::var("SCX_SYSFS_PREFIX").unwrap_or("".to_string());
63}
64
65fn load_vmlinux_btf() -> &'static mut btf {
66    let btf = unsafe { btf__load_vmlinux_btf() };
67    if btf.is_null() {
68        panic!("btf__load_vmlinux_btf() returned NULL, was CONFIG_DEBUG_INFO_BTF enabled?")
69    }
70    unsafe { &mut *btf }
71}
72
73lazy_static::lazy_static! {
74    static ref VMLINUX_BTF: &'static mut btf = load_vmlinux_btf();
75}
76
77fn btf_kind(t: &btf_type) -> u32 {
78    (t.info >> 24) & 0x1f
79}
80
81fn btf_vlen(t: &btf_type) -> u32 {
82    t.info & 0xffff
83}
84
85fn btf_type_plus_1(t: &btf_type) -> *const c_void {
86    let ptr_val = t as *const btf_type as usize;
87    (ptr_val + size_of::<btf_type>()) as *const c_void
88}
89
90fn btf_enum(t: &btf_type) -> &[btf_enum] {
91    let ptr = btf_type_plus_1(t);
92    unsafe { from_raw_parts(ptr as *const btf_enum, btf_vlen(t) as usize) }
93}
94
95fn btf_enum64(t: &btf_type) -> &[btf_enum64] {
96    let ptr = btf_type_plus_1(t);
97    unsafe { from_raw_parts(ptr as *const btf_enum64, btf_vlen(t) as usize) }
98}
99
100fn btf_members(t: &btf_type) -> &[btf_member] {
101    let ptr = btf_type_plus_1(t);
102    unsafe { from_raw_parts(ptr as *const btf_member, btf_vlen(t) as usize) }
103}
104
105fn btf_params(t: &btf_type) -> &[btf_param] {
106    let ptr = btf_type_plus_1(t);
107    unsafe { from_raw_parts(ptr as *const btf_param, btf_vlen(t) as usize) }
108}
109
110fn btf_name_str_by_offset(btf: &btf, name_off: u32) -> Result<&str> {
111    let n = unsafe { btf__name_by_offset(btf, name_off) };
112    if n.is_null() {
113        bail!("btf__name_by_offset() returned NULL");
114    }
115    unsafe { CStr::from_ptr(n) }
116        .to_str()
117        .with_context(|| format!("Failed to convert {:?} to string", n))
118}
119
120/// Recover the true value of a 64-bit enum enumerator whose kernel BTF entry
121/// was truncated to its low 32 bits.
122///
123/// Kernels whose BTF was generated without BTF_KIND_ENUM64 support encode
124/// 64-bit enums as 8-byte BTF_KIND_ENUM entries whose enumerator values only
125/// carry the low 32 bits. This happens with pahole < 1.24, which predates
126/// ENUM64, and with pahole passing --skip_encoding_btf_enum64 (e.g. Google's
127/// Container-Optimized OS / GKE kernels deliberately pass it for backward
128/// compatibility with older BTF consumers). The high bits
129/// can't be recovered from kernel BTF, so substitute the value from the
130/// vmlinux.h this tree was built against, cross-checked against the low 32
131/// bits the kernel did provide.
132///
133/// Note that this is a best-effort recovery, not a ground truth. The
134/// substitution assumes the running kernel agrees with this tree's vmlinux.h
135/// on the high 32 bits, but only the low 32 bits can actually be verified.
136/// The cross-check is vacuous for enumerators whose value has no low bits
137/// set (e.g. SCX_DSQ_FLAG_BUILTIN, __SCX_ENQ_INTERNAL_MASK,
138/// SCX_ENQ_CLEAR_OPSS, SCX_ECODE_*): their lo32 is 0 and matches anything,
139/// so those substitutions rest entirely on the high bits never moving. An
140/// enumerator missing from the table (a kernel newer than this tree's
141/// vmlinux.h, or a stale autogen table) can't be recovered at all. If a
142/// substitution is ever wrong, the scheduler operates on bogus values (e.g.
143/// dispatching to nonexistent DSQ ids or silently dropping flags) and can
144/// wildly malfunction, which is why the mismatch and table-miss paths refuse
145/// instead of guessing.
146fn recover_truncated_enum64_from(
147    table: &[(&str, &str, u64)],
148    type_name: &str,
149    name: &str,
150    lo32: u32,
151) -> Result<u64> {
152    static WARN_ONCE: std::sync::Once = std::sync::Once::new();
153
154    let Some(&(_, _, abi_val)) = table.iter().find(|(t, n, _)| *t == type_name && *n == name)
155    else {
156        // Unknown enumerator (likely a stale autogen table). Fail
157        // pessimistically to avoid returning an invalid value. Log too, as
158        // callers commonly swallow the error with .unwrap_or(0).
159        let msg = format!(
160            "kernel BTF truncates 64-bit enum {}::{} to 0x{:x}; 64-bit \
161             variant not found in vmlinux.h",
162            type_name, name, lo32
163        );
164        error!("{}", msg);
165        bail!(msg);
166    };
167
168    if abi_val <= u32::MAX as u64 {
169        return Ok(lo32 as u64);
170    }
171
172    if abi_val as u32 != lo32 {
173        // Log too, as callers commonly swallow the error with .unwrap_or(0).
174        let msg = format!(
175            "kernel BTF value of {}::{} (0x{:x}) doesn't match the low 32 bits \
176             of the vmlinux.h value (0x{:x}); refusing to substitute",
177            type_name, name, lo32, abi_val
178        );
179        error!("{}", msg);
180        bail!(msg);
181    }
182
183    WARN_ONCE.call_once(|| {
184        warn!(
185            "kernel BTF lacks BTF_KIND_ENUM64 encoding (generated by \
186             pahole < 1.24 or with --skip_encoding_btf_enum64), so 64-bit \
187             scx enum values are truncated to their low 32 bits in kernel \
188             BTF. Substituting the full 64-bit values from the vmlinux.h \
189             this binary was built against, cross-checked against the low \
190             32 bits the kernel does provide. The high 32 bits cannot be \
191             verified: if the running kernel's actual values differ from \
192             the build-time vmlinux.h (e.g. an enum that moved in a newer \
193             kernel), the scheduler will operate on bogus values, such as \
194             dispatching to nonexistent DSQ ids, and can wildly malfunction."
195        );
196    });
197    Ok(abi_val)
198}
199
200fn recover_truncated_enum64(type_name: &str, name: &str, lo32: u32) -> Result<u64> {
201    recover_truncated_enum64_from(enums_abi::ENUM_ABI_VALUES, type_name, name, lo32)
202}
203
204pub fn read_enum(type_name: &str, name: &str) -> Result<u64> {
205    let btf: &btf = *VMLINUX_BTF;
206
207    let c_type_name = CString::new(type_name).unwrap();
208    let tid = unsafe { btf__find_by_name(btf, c_type_name.as_ptr()) };
209    if tid < 0 {
210        bail!("type {:?} doesn't exist, ret={}", type_name, tid);
211    }
212
213    let t = unsafe { btf__type_by_id(btf, tid as _) };
214    if t.is_null() {
215        bail!("btf__type_by_id({}) returned NULL", tid);
216    }
217    let t = unsafe { &*t };
218
219    match btf_kind(t) {
220        BTF_KIND_ENUM => {
221            for e in btf_enum(t).iter() {
222                if btf_name_str_by_offset(btf, e.name_off)? == name {
223                    // Try to recover a 64-bit enum from an 8-byte BTF_KIND_ENUM that was encoded
224                    // without ENUM64 support (old pahole or --skip_encoding_btf_enum64). Only
225                    // scx_* types are covered by the substitution table; non-scx types fall
226                    // through to the raw value so this generic utility keeps working for them.
227                    if unsafe { t.__bindgen_anon_1.size } == 8 && type_name.starts_with("scx_") {
228                        return recover_truncated_enum64(type_name, name, e.val as u32);
229                    }
230                    return Ok(e.val as u64);
231                }
232            }
233        }
234        BTF_KIND_ENUM64 => {
235            for e in btf_enum64(t).iter() {
236                if btf_name_str_by_offset(btf, e.name_off)? == name {
237                    return Ok(((e.val_hi32 as u64) << 32) | (e.val_lo32) as u64);
238                }
239            }
240        }
241        _ => (),
242    }
243
244    Err(anyhow!("{:?} doesn't exist in {:?}", name, type_name))
245}
246
247/// Read an enum value from the first BTF enum type that contains it.
248pub fn read_enum_any(type_names: &[&str], name: &str) -> Result<u64> {
249    let mut errors = Vec::new();
250
251    for type_name in type_names {
252        match read_enum(type_name, name) {
253            Ok(val) => return Ok(val),
254            Err(err) => errors.push(format!("{}: {:#}", type_name, err)),
255        }
256    }
257
258    bail!(
259        "{:?} doesn't exist in any of {:?}: {}",
260        name,
261        type_names,
262        errors.join("; ")
263    )
264}
265
266pub fn struct_has_field(type_name: &str, field: &str) -> Result<bool> {
267    let btf: &btf = *VMLINUX_BTF;
268
269    let c_type_name = CString::new(type_name).unwrap();
270    let tid = unsafe { btf__find_by_name_kind(btf, c_type_name.as_ptr(), BTF_KIND_STRUCT) };
271    if tid < 0 {
272        bail!("type {:?} doesn't exist, ret={}", type_name, tid);
273    }
274
275    let t = unsafe { btf__type_by_id(btf, tid as _) };
276    if t.is_null() {
277        bail!("btf__type_by_id({}) returned NULL", tid);
278    }
279    let t = unsafe { &*t };
280
281    for m in btf_members(t).iter() {
282        if btf_name_str_by_offset(btf, m.name_off)? == field {
283            return Ok(true);
284        }
285    }
286
287    Ok(false)
288}
289
290pub fn ksym_exists(ksym: &str) -> Result<bool> {
291    let btf: &btf = *VMLINUX_BTF;
292
293    let ksym_name = CString::new(ksym).unwrap();
294    let tid = unsafe { btf__find_by_name(btf, ksym_name.as_ptr()) };
295    Ok(tid >= 0)
296}
297
298/// A raw BPF instruction for the load probes below, laid out as the kernel
299/// reads it: the two register nibbles follow the host's bitfield order.
300#[repr(C)]
301#[derive(Clone, Copy)]
302struct ProbeInsn {
303    code: u8,
304    regs: u8,
305    off: i16,
306    imm: i32,
307}
308
309const fn probe_insn(code: u8, dst: u8, src: u8, off: i16, imm: i32) -> ProbeInsn {
310    #[cfg(target_endian = "little")]
311    let regs = (src << 4) | dst;
312    #[cfg(target_endian = "big")]
313    let regs = (dst << 4) | src;
314    ProbeInsn {
315        code,
316        regs,
317        off,
318        imm,
319    }
320}
321
322// Opcodes composed from linux/bpf_common.h and linux/bpf.h.
323const PROBE_LD_IMM64: u8 = 0x18; // BPF_LD | BPF_DW | BPF_IMM
324const PROBE_MOV64_REG: u8 = 0xbf; // BPF_ALU64 | BPF_MOV | BPF_X
325const PROBE_MOV64_IMM: u8 = 0xb7; // BPF_ALU64 | BPF_MOV | BPF_K
326const PROBE_ATOMIC_DW: u8 = 0xdb; // BPF_STX | BPF_ATOMIC | BPF_DW
327const PROBE_EXIT: u8 = 0x95; // BPF_JMP | BPF_EXIT
328const PROBE_ATOMIC_OR_FETCH: i32 = 0x41; // BPF_OR | BPF_FETCH
329const PROBE_ADDR_SPACE_CAST: i16 = 1;
330
331/// Load `insns` as a GPL program of `prog_type` with the BPF_F_* `flags`.
332fn probe_prog_load(
333    prog_type: bpf_prog_type,
334    flags: u32,
335    insns: &[ProbeInsn],
336) -> io::Result<OwnedFd> {
337    let mut opts = bpf_prog_load_opts {
338        sz: size_of::<bpf_prog_load_opts>() as _,
339        prog_flags: flags,
340        ..Default::default()
341    };
342    let fd = unsafe {
343        bpf_prog_load(
344            prog_type,
345            std::ptr::null(),
346            c"GPL".as_ptr(),
347            insns.as_ptr() as *const bpf_insn,
348            insns.len() as _,
349            &mut opts,
350        )
351    };
352    if fd < 0 {
353        return Err(io::Error::from_raw_os_error(-fd));
354    }
355    Ok(unsafe { OwnedFd::from_raw_fd(fd) })
356}
357
358/// A fetching OR on arena memory, which the verifier rejects where the JIT has
359/// no lowering for it. The JITs accept the fetching AND, OR and XOR as a
360/// group, so one probe covers all three. The arena has to be mapped before
361/// the load because the verifier requires its user address to be known.
362fn probe_arena_fetch_bitops() -> bool {
363    let opts = bpf_map_create_opts {
364        sz: size_of::<bpf_map_create_opts>() as _,
365        map_flags: BPF_F_MMAPABLE,
366        ..Default::default()
367    };
368    let map_fd = unsafe { bpf_map_create(BPF_MAP_TYPE_ARENA, std::ptr::null(), 0, 0, 1, &opts) };
369    if map_fd < 0 {
370        return false;
371    }
372    let map = unsafe { OwnedFd::from_raw_fd(map_fd) };
373    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
374    let addr = unsafe {
375        libc::mmap(
376            std::ptr::null_mut(),
377            page,
378            libc::PROT_READ | libc::PROT_WRITE,
379            libc::MAP_SHARED,
380            map.as_raw_fd(),
381            0,
382        )
383    };
384    if addr == libc::MAP_FAILED {
385        return false;
386    }
387
388    let insns = [
389        probe_insn(
390            PROBE_LD_IMM64,
391            1,
392            BPF_PSEUDO_MAP_VALUE as u8,
393            0,
394            map.as_raw_fd(),
395        ),
396        probe_insn(0, 0, 0, 0, 0),
397        probe_insn(PROBE_MOV64_REG, 1, 1, PROBE_ADDR_SPACE_CAST, 1),
398        probe_insn(PROBE_MOV64_IMM, 2, 0, 0, 1),
399        probe_insn(PROBE_ATOMIC_DW, 1, 2, 0, PROBE_ATOMIC_OR_FETCH),
400        probe_insn(PROBE_MOV64_IMM, 0, 0, 0, 0),
401        probe_insn(PROBE_EXIT, 0, 0, 0, 0),
402    ];
403    let loaded = probe_prog_load(BPF_PROG_TYPE_SYSCALL, BPF_F_SLEEPABLE, &insns).is_ok();
404    unsafe { libc::munmap(addr, page) };
405    loaded
406}
407
408// BPF_PROG_STREAM_OPEN and its attributes, absent from the pinned libbpf. The
409// command number comes from the running kernel's BTF and is None on a kernel
410// without the command.
411const BPF_F_STREAM_NONBLOCK: u32 = 1;
412
413lazy_static::lazy_static! {
414    static ref BPF_PROG_STREAM_OPEN: Option<u64> =
415        read_enum("bpf_cmd", "BPF_PROG_STREAM_OPEN").ok();
416}
417
418#[repr(C)]
419struct ProgStreamOpenAttr {
420    prog_fd: u32,
421    stream_id: u32,
422    flags: u32,
423}
424
425/// Open a read-only descriptor on one of `prog`'s streams, `stream_id` being
426/// BPF_STREAM_STDOUT or BPF_STREAM_STDERR. Reads block unless `nonblock`.
427/// poll(2) reports POLLIN for data and POLLHUP once the program is freed, with
428/// buffered data still readable. A kernel without the command fails with
429/// EINVAL.
430pub fn prog_stream_open(
431    prog: BorrowedFd<'_>,
432    stream_id: u32,
433    nonblock: bool,
434) -> io::Result<OwnedFd> {
435    let attr = ProgStreamOpenAttr {
436        prog_fd: prog.as_raw_fd() as u32,
437        stream_id,
438        flags: if nonblock { BPF_F_STREAM_NONBLOCK } else { 0 },
439    };
440    let Some(cmd) = *BPF_PROG_STREAM_OPEN else {
441        return Err(io::Error::from_raw_os_error(libc::EINVAL));
442    };
443    let fd = unsafe {
444        libc::syscall(
445            libc::SYS_bpf,
446            cmd as libc::c_long,
447            &attr as *const ProgStreamOpenAttr as *const c_void,
448            size_of::<ProgStreamOpenAttr>(),
449        )
450    };
451    if fd < 0 {
452        return Err(io::Error::last_os_error());
453    }
454    Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
455}
456
457fn probe_prog_stream_open() -> bool {
458    let insns = [
459        probe_insn(PROBE_MOV64_IMM, 0, 0, 0, 0),
460        probe_insn(PROBE_EXIT, 0, 0, 0, 0),
461    ];
462    let Ok(prog) = probe_prog_load(BPF_PROG_TYPE_SYSCALL, BPF_F_SLEEPABLE, &insns) else {
463        return false;
464    };
465    prog_stream_open(prog.as_fd(), BPF_STREAM_STDOUT, true).is_ok()
466}
467
468// Bits of the scx_lib_features rodata word, mirroring enum scx_lib_feature in
469// scheds/include/scx/features.bpf.h.
470pub const SCX_LIB_FEAT_ARENA_FETCH_BITOPS: u64 = 1 << 0;
471
472lazy_static::lazy_static! {
473    /// The JIT lowers fetching AND, OR and XOR on arena pointers.
474    pub static ref ARENA_FETCH_BITOPS: bool = probe_arena_fetch_bitops();
475
476    /// prog_stream_open() works, so BPF streams can be polled.
477    pub static ref PROG_STREAM_OPEN_SUPPORTED: bool = probe_prog_stream_open();
478
479    /// Every probed kernel feature: its name, its bit in the scx_lib_features
480    /// rodata word or zero when only userspace consults it, and whether the
481    /// running kernel has it.
482    pub static ref LIB_FEATURES: Vec<(&'static str, u64, bool)> = vec![
483        ("arena_fetch_bitops", SCX_LIB_FEAT_ARENA_FETCH_BITOPS, *ARENA_FETCH_BITOPS),
484        ("prog_stream_fds", 0, *PROG_STREAM_OPEN_SUPPORTED),
485    ];
486}
487
488/// Offset and size of the rodata variable `name` in an open object's BTF,
489/// None when the object has no such variable.
490fn rodata_var(obj: &OpenObject, name: &str) -> Result<Option<(usize, usize)>> {
491    let raw = unsafe { &*obj.as_libbpf_object().as_ptr() };
492    let Some(btf) = libbpf_rs::btf::Btf::from_bpf_object(raw)? else {
493        return Ok(None);
494    };
495    let mut var_off = None;
496    for sec in btf.type_by_kind::<libbpf_rs::btf::types::DataSec<'_>>() {
497        if sec.name() != Some(OsStr::new(".rodata")) {
498            continue;
499        }
500        for var in sec.iter() {
501            let ty: libbpf_rs::btf::types::Var<'_> = btf
502                .type_by_id(var.ty)
503                .ok_or_else(|| anyhow!("BTF datasec entry {:?} is not a variable", var.ty))?;
504            if ty.name() == Some(OsStr::new(name)) {
505                var_off = Some((var.offset as usize, var.size));
506            }
507        }
508    }
509    Ok(var_off)
510}
511
512/// Whether an open object has the rodata variable `name`.
513pub fn has_rodata_var(obj: &OpenObject, name: &str) -> Result<bool> {
514    Ok(rodata_var(obj, name)?.is_some())
515}
516
517/// Write `value` over the rodata variable `name` of an open object. Returns
518/// false when the object has no such variable.
519pub fn set_rodata_var(obj: &mut OpenObject, name: &str, value: &[u8]) -> Result<bool> {
520    let Some((off, size)) = rodata_var(obj, name)? else {
521        return Ok(false);
522    };
523    if size != value.len() {
524        bail!(
525            "rodata variable {name} is {size} bytes, not {}",
526            value.len()
527        );
528    }
529    for mut map in obj.maps_mut() {
530        if !map.name().to_string_lossy().ends_with(".rodata") {
531            continue;
532        }
533        let data = map
534            .initial_value_mut()
535            .ok_or_else(|| anyhow!("rodata map has no initial value"))?;
536        let Some(slot) = data.get_mut(off..off + size) else {
537            bail!(
538                "rodata variable {name} lies outside the {} byte map",
539                data.len()
540            );
541        };
542        slot.copy_from_slice(value);
543        return Ok(true);
544    }
545    bail!("object has no .rodata map for {name}")
546}
547
548/// Hand the probed features to the BPF side of an open object and log them,
549/// at info when the kernel lacks any of them and at debug otherwise.
550pub fn set_lib_features(obj: &mut OpenObject) -> Result<()> {
551    let mut bits = 0;
552    let mut missing = false;
553    let mut line = String::new();
554    for (name, bit, present) in LIB_FEATURES.iter() {
555        if *present {
556            bits |= bit;
557        } else {
558            missing = true;
559        }
560        line.push_str(&format!(" {name}={}", if *present { "yes" } else { "no" }));
561    }
562    set_rodata_var(obj, "scx_lib_features", &bits.to_ne_bytes())?;
563    if missing {
564        info!("kernel features:{line}");
565    } else {
566        debug!("kernel features:{line}");
567    }
568    Ok(())
569}
570
571/// Scan the running kernel's vmlinux BTF for scx kfuncs whose public-facing
572/// prototype still carries the implicit `aux` (struct bpf_prog_aux *) argument.
573///
574/// This is the KF_IMPLICIT_ARGS-on-pahole-<1.26 bug: pahole < 1.26 fails to
575/// split such kfuncs into a public prototype (without `aux`) and an `_impl`
576/// variant (with it), so the visible prototype keeps `aux`. BPF programs that
577/// declare the kfunc without it then fail to load with the confusing
578/// 'func_proto incompatible with vmlinux' error. Returns the names of the
579/// affected kfuncs, so a clear diagnostic can be produced on load failure.
580pub fn malformed_scx_kfuncs() -> Vec<String> {
581    let btf: &btf = *VMLINUX_BTF;
582    let mut bad = Vec::new();
583    let cnt = unsafe { btf__type_cnt(btf) };
584
585    for id in 1..cnt {
586        let t = unsafe { btf__type_by_id(btf, id) };
587        if t.is_null() {
588            continue;
589        }
590        let t = unsafe { &*t };
591        if btf_kind(t) != BTF_KIND_FUNC {
592            continue;
593        }
594
595        let Ok(name) = btf_name_str_by_offset(btf, t.name_off) else {
596            continue;
597        };
598        if !(name.starts_with("scx_bpf_") || name.starts_with("__scx_bpf_")) {
599            continue;
600        }
601        // The implicit `aux` argument legitimately appears on the `_impl`
602        // variant that resolve_btfids splits off; only the public-facing name
603        // (without the `_impl` suffix) should be free of it. On a broken kernel
604        // the split never happens, so there is no `_impl` variant and the
605        // public name itself keeps `aux`.
606        if name.ends_with("_impl") {
607            continue;
608        }
609
610        // FUNC -> FUNC_PROTO
611        let proto_id = unsafe { t.__bindgen_anon_1.type_ };
612        let pt = unsafe { btf__type_by_id(btf, proto_id) };
613        if pt.is_null() {
614            continue;
615        }
616        let pt = unsafe { &*pt };
617        if btf_kind(pt) != BTF_KIND_FUNC_PROTO {
618            continue;
619        }
620
621        if btf_params(pt).iter().any(|p| {
622            p.name_off != 0 && matches!(btf_name_str_by_offset(btf, p.name_off), Ok("aux"))
623        }) {
624            bad.push(name.to_string());
625        }
626    }
627
628    bad
629}
630
631pub fn in_kallsyms(ksym: &str) -> Result<bool> {
632    let file = std::fs::File::open("/proc/kallsyms")?;
633    let reader = std::io::BufReader::new(file);
634
635    for line in reader.lines() {
636        for sym in line.unwrap().split_whitespace() {
637            if ksym == sym {
638                return Ok(true);
639            }
640        }
641    }
642
643    Ok(false)
644}
645
646/// Returns the mount point for a filesystem type.
647pub fn get_fs_mount(mount_type: &str) -> Result<Vec<std::path::PathBuf>> {
648    let proc_mounts_path = std::path::Path::new(PROCFS_MOUNTS);
649
650    let file = std::fs::File::open(proc_mounts_path)
651        .with_context(|| format!("Failed to open {}", proc_mounts_path.display()))?;
652
653    let reader = BufReader::new(file);
654
655    let mut mounts = Vec::new();
656    for line in reader.lines() {
657        let line = line.context("Failed to read line from /proc/mounts")?;
658        let mount_info: Vec<&str> = line.split_whitespace().collect();
659
660        if mount_info.len() > 3 && mount_info[2] == mount_type {
661            let mount_path = std::path::PathBuf::from(mount_info[1]);
662            mounts.push(mount_path);
663        }
664    }
665
666    Ok(mounts)
667}
668
669/// Returns the tracefs mount point.
670pub fn tracefs_mount() -> Result<std::path::PathBuf> {
671    let mounts = get_fs_mount(TRACEFS)?;
672    mounts.into_iter().next().context("No tracefs mount found")
673}
674
675/// Returns the debugfs mount point.
676pub fn debugfs_mount() -> Result<std::path::PathBuf> {
677    let mounts = get_fs_mount(DEBUGFS)?;
678    mounts.into_iter().next().context("No debugfs mount found")
679}
680
681pub fn tracer_available(tracer: &str) -> Result<bool> {
682    let base_path = tracefs_mount().unwrap_or_else(|_| debugfs_mount().unwrap().join("tracing"));
683    let file = match std::fs::File::open(base_path.join("available_tracers")) {
684        Ok(f) => f,
685        Err(_) => return Ok(false),
686    };
687    let reader = std::io::BufReader::new(file);
688
689    for line in reader.lines() {
690        for tc in line.unwrap().split_whitespace() {
691            if tracer == tc {
692                return Ok(true);
693            }
694        }
695    }
696
697    Ok(false)
698}
699
700pub fn tracepoint_exists(tracepoint: &str) -> Result<bool> {
701    let base_path = tracefs_mount().unwrap_or_else(|_| debugfs_mount().unwrap().join("tracing"));
702    let file = match std::fs::File::open(base_path.join("available_events")) {
703        Ok(f) => f,
704        Err(_) => return Ok(false),
705    };
706    let reader = std::io::BufReader::new(file);
707
708    for line in reader.lines() {
709        for tp in line.unwrap().split_whitespace() {
710            if tracepoint == tp {
711                return Ok(true);
712            }
713        }
714    }
715
716    Ok(false)
717}
718
719/// Whether the kernel supports function tracing (CONFIG_FUNCTION_TRACER),
720/// needed for fentry/fexit BPF programs to load or attach.
721/// `/proc/sys/kernel/ftrace_enabled` only exists when it's compiled in,
722/// so its presence is a cheap proxy.
723pub fn function_tracer_available() -> bool {
724    std::path::Path::new("/proc/sys/kernel/ftrace_enabled").exists()
725}
726
727pub fn cond_kprobe_enable<T>(sym: &str, prog_ptr: &OpenProgramImpl<T>) -> Result<bool> {
728    if in_kallsyms(sym)? {
729        unsafe {
730            bpf_program__set_autoload(prog_ptr.as_libbpf_object().as_ptr(), true);
731        }
732        return Ok(true);
733    } else {
734        warn!("symbol {sym} is missing, kprobe not loaded");
735    }
736
737    Ok(false)
738}
739
740pub fn cond_kprobes_enable<T>(kprobes: Vec<(&str, &OpenProgramImpl<T>)>) -> Result<bool> {
741    // Check if all the symbols exist.
742    for (sym, _) in kprobes.iter() {
743        if !in_kallsyms(sym)? {
744            warn!("symbol {sym} is missing, kprobe not loaded");
745            return Ok(false);
746        }
747    }
748
749    // Enable all the tracepoints.
750    for (_, ptr) in kprobes.iter() {
751        unsafe {
752            bpf_program__set_autoload(ptr.as_libbpf_object().as_ptr(), true);
753        }
754    }
755
756    Ok(true)
757}
758
759pub fn cond_kprobe_load<T>(sym: &str, prog_ptr: &OpenProgramImpl<T>) -> Result<bool> {
760    if in_kallsyms(sym)? {
761        unsafe {
762            bpf_program__set_autoload(prog_ptr.as_libbpf_object().as_ptr(), true);
763            bpf_program__set_autoattach(prog_ptr.as_libbpf_object().as_ptr(), false);
764        }
765        return Ok(true);
766    } else {
767        warn!("symbol {sym} is missing, kprobe not loaded");
768    }
769
770    Ok(false)
771}
772
773pub fn cond_kprobe_attach<T>(sym: &str, prog_ptr: &ProgramImpl<T>) -> Result<bool> {
774    if in_kallsyms(sym)? {
775        unsafe {
776            bpf_program__attach(prog_ptr.as_libbpf_object().as_ptr());
777        }
778        return Ok(true);
779    } else {
780        warn!("symbol {sym} is missing, kprobe not loaded");
781    }
782
783    Ok(false)
784}
785
786pub fn cond_tracepoint_enable<T>(tracepoint: &str, prog_ptr: &OpenProgramImpl<T>) -> Result<bool> {
787    if tracepoint_exists(tracepoint)? {
788        unsafe {
789            bpf_program__set_autoload(prog_ptr.as_libbpf_object().as_ptr(), true);
790        }
791        return Ok(true);
792    } else {
793        warn!("tracepoint {tracepoint} is missing, tracepoint not loaded");
794    }
795
796    Ok(false)
797}
798
799pub fn cond_tracepoints_enable<T>(tracepoints: Vec<(&str, &OpenProgramImpl<T>)>) -> Result<bool> {
800    // Check if all the tracepoints exist.
801    for (tp, _) in tracepoints.iter() {
802        if !tracepoint_exists(tp)? {
803            warn!("tracepoint {tp} is missing, tracepoint not loaded");
804            return Ok(false);
805        }
806    }
807
808    // Enable all the tracepoints.
809    for (_, ptr) in tracepoints.iter() {
810        unsafe {
811            bpf_program__set_autoload(ptr.as_libbpf_object().as_ptr(), true);
812        }
813    }
814
815    Ok(true)
816}
817
818pub fn is_sched_ext_enabled() -> io::Result<bool> {
819    let content = std::fs::read_to_string("/sys/kernel/sched_ext/state")?;
820
821    match content.trim() {
822        "enabled" => Ok(true),
823        "disabled" => Ok(false),
824        _ => {
825            // Error if the content is neither "enabled" nor "disabled"
826            Err(io::Error::new(
827                io::ErrorKind::InvalidData,
828                "Unexpected content in /sys/kernel/sched_ext/state",
829            ))
830        }
831    }
832}
833
834#[macro_export]
835macro_rules! unwrap_or_break {
836    ($expr: expr, $label: lifetime) => {{
837        match $expr {
838            Ok(val) => val,
839            Err(e) => break $label Err(e),
840        }
841    }};
842}
843
844pub fn check_min_requirements(ops_struct: &str) -> Result<()> {
845    // ec7e3b0463e1 ("implement-ops") in https://github.com/sched-ext/sched_ext
846    // is the current minimum required kernel version.
847    if let Ok(false) | Err(_) = struct_has_field(ops_struct, "dump") {
848        bail!("{}.dump() missing, kernel too old?", ops_struct);
849    }
850    Ok(())
851}
852
853#[doc(hidden)]
854#[rustfmt::skip]
855#[macro_export]
856macro_rules! __scx_ops_open {
857    ($builder: expr, $obj_ref: expr, $ops: ident, $ops_struct: literal, $open_opts: expr) => { 'block: {
858        scx_utils::paste! {
859        scx_utils::unwrap_or_break!(scx_utils::compat::check_min_requirements($ops_struct), 'block);
860            use ::anyhow::Context;
861            use ::libbpf_rs::skel::OpenSkel;
862            use ::libbpf_rs::skel::SkelBuilder;
863
864            let mut skel = match $open_opts {
865                Some(opts_ref) => { // Match a reference directly
866                    match $builder.open_opts(opts_ref, $obj_ref).context("Failed to open BPF program with options") {
867                        Ok(val) => val,
868                        Err(e) => break 'block Err(e),
869                    }
870                }
871                None => {
872                    match $builder.open($obj_ref).context("Failed to open BPF program") {
873                        Ok(val) => val,
874                        Err(e) => break 'block Err(e),
875                    }
876                }
877            };
878
879            if let Err(e) = scx_utils::compat::set_lib_features(skel.open_object_mut()) {
880                break 'block Err(e);
881            }
882
883            let ops = skel.struct_ops.[<$ops _mut>]();
884            let path = std::path::Path::new("/sys/kernel/sched_ext/hotplug_seq");
885
886            let val = match std::fs::read_to_string(&path) {
887                Ok(val) => val,
888                Err(_) => {
889                    break 'block Err(anyhow::anyhow!("Failed to open or read file {:?}", path));
890                }
891            };
892
893            ops.hotplug_seq = match val.trim().parse::<u64>() {
894                Ok(parsed) => parsed,
895                Err(_) => {
896                    break 'block Err(anyhow::anyhow!("Failed to parse hotplug seq {}", val));
897                }
898            };
899
900            if let Ok(s) = ::std::env::var("SCX_TIMEOUT_MS") {
901                skel.struct_ops.[<$ops _mut>]().timeout_ms = match s.parse::<u32>() {
902                    Ok(ms) => {
903                        ::scx_utils::info!("Setting timeout_ms to {} based on environment", ms);
904                        ms
905                    },
906                    Err(e) => {
907                        break 'block anyhow::Result::Err(e).context("SCX_TIMEOUT_MS has invalid value");
908                    },
909                };
910            }
911
912            {
913                let ops = skel.struct_ops.[<$ops _mut>]();
914
915                let name_field = &mut ops.name;
916
917                let version_suffix = ::scx_utils::build_id::ops_version_suffix(env!("CARGO_PKG_VERSION"));
918                let bytes = version_suffix.as_bytes();
919                let mut i = 0;
920                let mut bytes_idx = 0;
921                let mut found_null = false;
922
923                while i < name_field.len() - 1 {
924                    found_null |= name_field[i] == 0;
925                    if !found_null {
926                        i += 1;
927                        continue;
928                    }
929
930                    if bytes_idx < bytes.len() {
931                        name_field[i] = bytes[bytes_idx] as i8;
932                        bytes_idx += 1;
933                    } else {
934                        break;
935                    }
936                    i += 1;
937                }
938                name_field[i] = 0;
939            }
940
941            $crate::import_enums!(skel);
942
943            let result = ::anyhow::Result::Ok(skel);
944
945            result
946        }
947    }};
948}
949
950/// struct sched_ext_ops can change over time. If compat.bpf.h::SCX_OPS_DEFINE()
951/// is used to define ops, and scx_ops_open!(), scx_ops_load!(), and
952/// scx_ops_attach!() are used to open, load and attach it, backward
953/// compatibility is automatically maintained where reasonable.
954#[macro_export]
955macro_rules! scx_ops_open {
956    ($builder: expr, $obj_ref: expr, $ops: ident, $open_opts: expr) => {
957        $crate::__scx_ops_open!($builder, $obj_ref, $ops, "sched_ext_ops", $open_opts)
958    };
959}
960
961/// BPF_PROG_LOAD flag under which a load or store through a plain scalar is an
962/// arena access, from 4c651a91bdfc in bpf-next. Not yet in the uapi header
963/// libbpf-sys 1.7 ships.
964pub const BPF_F_ARENA_SCALAR: u32 = 1 << 8;
965
966/// Open a cid-form (struct sched_ext_ops_cid) skeleton. Pair with
967/// scx_ops_cid_load!(), which skips the fix-ups for the cpu-form-only cgroup
968/// callbacks that don't exist in the cid form.
969///
970/// A cid-form scheduler registers through bpf_scx_reg_cid() rather than
971/// bpf_scx_reg(), so common.bpf.h's prolog probe gets repointed at it. Both
972/// the cid form and bpf_scx_reg_cid() appeared in v7.2, so a kernel that
973/// accepts this skeleton always has the symbol.
974///
975/// An object built with SCX_ARENA_SCALAR carries the __SCX_MARKER_arena_scalar
976/// rodata marker and is loaded with BPF_F_ARENA_SCALAR. One without the marker
977/// loads as before. The flag is in bpf-next (4c651a91bdfc) and due in v7.4, so
978/// a marked object needs a kernel that has it.
979#[macro_export]
980macro_rules! scx_ops_cid_open {
981    ($builder: expr, $obj_ref: expr, $ops: ident, $open_opts: expr) => {
982        $crate::__scx_ops_open!($builder, $obj_ref, $ops, "sched_ext_ops_cid", $open_opts).and_then(
983            |mut skel| {
984                skel.progs
985                    .scx_lib_init_probe
986                    .set_attach_target(0, Some("bpf_scx_reg_cid".to_string()))?;
987                if $crate::compat::has_rodata_var(
988                    ::libbpf_rs::skel::OpenSkel::open_object(&skel),
989                    "__SCX_MARKER_arena_scalar",
990                )? {
991                    for mut prog in
992                        ::libbpf_rs::skel::OpenSkel::open_object_mut(&mut skel).progs_mut()
993                    {
994                        // libbpf-rs has no flags getter on an open program
995                        let ptr = ::libbpf_rs::AsRawLibbpf::as_libbpf_object(&prog).as_ptr();
996                        let flags = unsafe { ::libbpf_rs::libbpf_sys::bpf_program__flags(ptr) };
997                        prog.set_flags(flags | $crate::compat::BPF_F_ARENA_SCALAR);
998                    }
999                }
1000                ::anyhow::Result::Ok(skel)
1001            },
1002        )
1003    };
1004}
1005
1006/// struct sched_ext_ops can change over time. If compat.bpf.h::SCX_OPS_DEFINE()
1007/// is used to define ops, and scx_ops_open!(), scx_ops_load!(), and
1008/// scx_ops_attach!() are used to open, load and attach it, backward
1009/// compatibility is automatically maintained where reasonable.
1010/*
1011 * Load-time fix-ups that only exist in the cpu ops form. The cid form renamed
1012 * the cgroup callbacks to cpuctl_*, so referencing these members would not
1013 * compile against a cid-form skeleton.
1014 */
1015#[doc(hidden)]
1016#[rustfmt::skip]
1017#[macro_export]
1018macro_rules! __scx_ops_form_fixups {
1019    (cpu, $ops_ref: expr) => {
1020        if !$ops_ref.cgroup_set_bandwidth.is_null() {
1021            if let Ok(false) | Err(_) = scx_utils::compat::struct_has_field("sched_ext_ops", "cgroup_set_bandwidth") {
1022                ::scx_utils::warn!("kernel doesn't support ops.cgroup_set_bandwidth()");
1023                $ops_ref.cgroup_set_bandwidth = std::ptr::null_mut();
1024            }
1025        }
1026        if !$ops_ref.cgroup_set_idle.is_null() {
1027            if let Ok(false) | Err(_) = scx_utils::compat::struct_has_field("sched_ext_ops", "cgroup_set_idle") {
1028                ::scx_utils::warn!("kernel doesn't support ops.cgroup_set_idle()");
1029                $ops_ref.cgroup_set_idle = std::ptr::null_mut();
1030            }
1031        }
1032    };
1033    (cid, $ops_ref: expr) => {};
1034}
1035
1036#[doc(hidden)]
1037#[rustfmt::skip]
1038#[macro_export]
1039macro_rules! __scx_ops_load {
1040    ($skel: expr, $ops: ident, $uei: ident, $form: ident) => { 'block: {
1041        scx_utils::paste! {
1042            use ::anyhow::Context;
1043            use ::libbpf_rs::skel::OpenSkel;
1044
1045            {
1046                let ops = $skel.struct_ops.[<$ops _mut>]();
1047                $crate::__scx_ops_form_fixups!($form, ops);
1048                if !ops.sub_attach.is_null() {
1049                    if let Ok(false) | Err(_) = scx_utils::compat::struct_has_field("sched_ext_ops", "sub_attach") {
1050                        ::scx_utils::warn!("kernel doesn't support ops.sub_attach()");
1051                        ops.sub_attach = std::ptr::null_mut();
1052                    }
1053                }
1054                if !ops.sub_detach.is_null() {
1055                    if let Ok(false) | Err(_) = scx_utils::compat::struct_has_field("sched_ext_ops", "sub_detach") {
1056                        ::scx_utils::warn!("kernel doesn't support ops.sub_detach()");
1057                        ops.sub_detach = std::ptr::null_mut();
1058                    }
1059                }
1060                if ops.sub_cgroup_id > 0 {
1061                    if let Ok(false) | Err(_) = scx_utils::compat::struct_has_field("sched_ext_ops", "sub_cgroup_id") {
1062                        ::scx_utils::warn!("kernel doesn't support ops.sub_cgroup_id");
1063                        ops.sub_cgroup_id = 0;
1064                    }
1065                }
1066                if ops.rescue_bandwidth_ppt > 0 {
1067                    if let Ok(false) | Err(_) = scx_utils::compat::struct_has_field("sched_ext_ops", "rescue_bandwidth_ppt") {
1068                        ::scx_utils::warn!("kernel doesn't support ops.rescue_bandwidth_ppt");
1069                        ops.rescue_bandwidth_ppt = 0;
1070                    }
1071                }
1072                if ops.rescue_quantum_us > 0 {
1073                    if let Ok(false) | Err(_) = scx_utils::compat::struct_has_field("sched_ext_ops", "rescue_quantum_us") {
1074                        ::scx_utils::warn!("kernel doesn't support ops.rescue_quantum_us");
1075                        ops.rescue_quantum_us = 0;
1076                    }
1077                }
1078            }
1079
1080            if !scx_utils::compat::function_tracer_available() {
1081                break 'block Err(anyhow::anyhow!("kernel is missing CONFIG_FUNCTION_TRACER"));
1082            }
1083
1084            scx_utils::uei_set_size!($skel, $ops, $uei);
1085            $skel.load().context("Failed to load BPF program").map_err(|e| {
1086                let bad = scx_utils::compat::malformed_scx_kfuncs();
1087                if bad.is_empty() {
1088                    e
1089                } else {
1090                    e.context(format!(
1091                        "the running kernel's BTF has malformed scx kfunc prototype(s): {}.\n\
1092                         \n\
1093                         These kfuncs are KF_IMPLICIT_ARGS but their public BTF prototype\n\
1094                         still carries the implicit 'struct bpf_prog_aux *' argument, which\n\
1095                         makes BPF programs fail to load with 'func_proto incompatible with\n\
1096                         vmlinux'. This happens when the kernel was built with pahole < 1.26.\n\
1097                         \n\
1098                         Fix: boot a kernel whose BTF was generated with pahole >= 1.26.\n\
1099                         Affected distros include Ubuntu 24.04 LTS. See kernel commit\n\
1100                         9edd04c4189e (\"docs: Raise minimum pahole version to 1.26 for\n\
1101                         KF_IMPLICIT_ARGS kfuncs\").",
1102                        bad.join(", ")
1103                    ))
1104                }
1105            }).inspect(|skel| {
1106                use ::libbpf_rs::skel::Skel;
1107                use ::libbpf_rs::AsRawLibbpf;
1108
1109                /*
1110                 * Associate non-struct_ops BPF programs with the scheduler's
1111                 * struct_ops map so that the kernel can determine which
1112                 * scheduler a BPF program belongs to. Association is required
1113                 * once a root scheduler declares ops.sub_attach(). Skip
1114                 * silently when the running kernel predates
1115                 * BPF_PROG_ASSOC_STRUCT_OPS; on supporting kernels, stop at
1116                 * the first failure to avoid warning for every program.
1117                 */
1118                if scx_utils::compat::read_enum("bpf_cmd", "BPF_PROG_ASSOC_STRUCT_OPS").is_ok() {
1119                    let map_ptr = skel.maps.$ops.as_libbpf_object().as_ptr();
1120                    for prog in skel.object().progs() {
1121                        if prog.prog_type() == ::libbpf_rs::ProgramType::StructOps || !prog.autoload() {
1122                            continue;
1123                        }
1124                        let ret = unsafe {
1125                            ::libbpf_rs::libbpf_sys::bpf_program__assoc_struct_ops(
1126                                prog.as_libbpf_object().as_ptr(), map_ptr, std::ptr::null_mut())
1127                        };
1128                        if ret < 0 {
1129                            ::scx_utils::warn!("Failed to associate {} with {}: {}",
1130                                               prog.name().to_string_lossy(), stringify!($ops), ret);
1131                            break;
1132                        }
1133                    }
1134                }
1135            })
1136        }
1137    }};
1138}
1139
1140/// struct sched_ext_ops can change over time. If compat.bpf.h::SCX_OPS_DEFINE()
1141/// is used to define ops, and scx_ops_open!(), scx_ops_load!(), and
1142/// scx_ops_attach!() are used to open, load and attach it, backward
1143/// compatibility is automatically maintained where reasonable.
1144#[macro_export]
1145macro_rules! scx_ops_load {
1146    ($skel: expr, $ops: ident, $uei: ident) => {
1147        $crate::__scx_ops_load!($skel, $ops, $uei, cpu)
1148    };
1149}
1150
1151/// Load a cid-form (struct sched_ext_ops_cid) skeleton opened with
1152/// scx_ops_cid_open!(). Skips the cpu-form-only cgroup callback fix-ups (the
1153/// cid form renamed them to cpuctl_*); the fix-ups for fields shared by both
1154/// forms still apply.
1155#[macro_export]
1156macro_rules! scx_ops_cid_load {
1157    ($skel: expr, $ops: ident, $uei: ident) => {
1158        $crate::__scx_ops_load!($skel, $ops, $uei, cid)
1159    };
1160}
1161
1162/// Must be used together with scx_ops_load!(). See there.
1163#[rustfmt::skip]
1164#[macro_export]
1165macro_rules! scx_ops_attach {
1166    ($skel: expr, $ops: ident) => {
1167        scx_ops_attach!($skel, $ops, false)
1168    };
1169    ($skel: expr, $ops: ident, $is_subsched: expr) => { 'block: {
1170        use ::anyhow::Context;
1171        use ::libbpf_rs::skel::Skel;
1172
1173        if !$is_subsched && scx_utils::compat::is_sched_ext_enabled().unwrap_or(false) {
1174            break 'block Err(anyhow::anyhow!(
1175                "another sched_ext scheduler is already running"
1176            ));
1177        }
1178        $skel
1179            .attach()
1180            .context("Failed to attach non-struct_ops BPF programs")
1181            .and_then(|_| {
1182                $skel
1183                    .maps
1184                    .$ops
1185                    .attach_struct_ops()
1186                    .context("Failed to attach struct_ops BPF programs")
1187            })
1188    }};
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193    #[test]
1194    fn test_read_enum() {
1195        assert_eq!(super::read_enum("pid_type", "PIDTYPE_TGID").unwrap(), 1);
1196    }
1197
1198    #[test]
1199    fn test_read_enum_any() {
1200        assert_eq!(
1201            super::read_enum_any(&["NO_SUCH_TYPE", "pid_type"], "PIDTYPE_TGID").unwrap(),
1202            1
1203        );
1204        assert!(super::read_enum_any(&["NO_SUCH_TYPE", "pid_type"], "NO_SUCH_ENUM").is_err());
1205    }
1206
1207    #[test]
1208    fn test_recover_truncated_enum64() {
1209        let table: &[(&str, &str, u64)] = &[
1210            ("scx_dsq_id_flags", "SCX_DSQ_LOCAL", 0x8000000000000002),
1211            ("scx_enq_flags", "SCX_ENQ_PREEMPT", 0x100000000),
1212            ("scx_enq_flags", "SCX_ENQ_HEAD", 0x10000),
1213        ];
1214
1215        // >32-bit values with matching low bits get substituted.
1216        assert_eq!(
1217            super::recover_truncated_enum64_from(table, "scx_dsq_id_flags", "SCX_DSQ_LOCAL", 2)
1218                .unwrap(),
1219            0x8000000000000002
1220        );
1221        assert_eq!(
1222            super::recover_truncated_enum64_from(table, "scx_enq_flags", "SCX_ENQ_PREEMPT", 0)
1223                .unwrap(),
1224            0x100000000
1225        );
1226        // Sub-32-bit values truncate losslessly, so the kernel's value stays
1227        // authoritative even when it disagrees with the table.
1228        assert_eq!(
1229            super::recover_truncated_enum64_from(table, "scx_enq_flags", "SCX_ENQ_HEAD", 0x20000)
1230                .unwrap(),
1231            0x20000
1232        );
1233        // A low-32 mismatch on a >32-bit value is ABI drift; refuse.
1234        assert!(
1235            super::recover_truncated_enum64_from(table, "scx_dsq_id_flags", "SCX_DSQ_LOCAL", 3)
1236                .is_err()
1237        );
1238        // Unknown enumerators fail pessimistically (stale autogen table).
1239        assert!(
1240            super::recover_truncated_enum64_from(table, "scx_enq_flags", "SCX_ENQ_NEW", 7).is_err()
1241        );
1242    }
1243
1244    #[test]
1245    fn test_enum_abi_table() {
1246        // Spot-check the autogenerated table against ABI values that have
1247        // been stable on every kernel that ships sched_ext.
1248        let find = |t: &str, n: &str| {
1249            super::enums_abi::ENUM_ABI_VALUES
1250                .iter()
1251                .find(|(ty, na, _)| *ty == t && *na == n)
1252                .map(|&(_, _, v)| v)
1253        };
1254        assert_eq!(
1255            find("scx_dsq_id_flags", "SCX_DSQ_FLAG_BUILTIN"),
1256            Some(1 << 63)
1257        );
1258        assert_eq!(
1259            find("scx_dsq_id_flags", "SCX_DSQ_LOCAL"),
1260            Some((1 << 63) | 2)
1261        );
1262        assert_eq!(
1263            find("scx_dsq_id_flags", "SCX_DSQ_LOCAL_ON"),
1264            Some((1 << 63) | (1 << 62))
1265        );
1266        assert_eq!(find("scx_public_consts", "SCX_SLICE_INF"), Some(u64::MAX));
1267        assert_eq!(find("scx_enq_flags", "SCX_ENQ_PREEMPT"), Some(1 << 32));
1268    }
1269
1270    #[test]
1271    fn test_struct_has_field() {
1272        assert!(super::struct_has_field("task_struct", "flags").unwrap());
1273        assert!(!super::struct_has_field("task_struct", "NO_SUCH_FIELD").unwrap());
1274        assert!(super::struct_has_field("NO_SUCH_STRUCT", "NO_SUCH_FIELD").is_err());
1275    }
1276
1277    #[test]
1278    fn test_ksym_exists() {
1279        assert!(super::ksym_exists("bpf_task_acquire").unwrap());
1280        assert!(!super::ksym_exists("NO_SUCH_KFUNC").unwrap());
1281    }
1282
1283    #[test]
1284    fn test_malformed_scx_kfuncs() {
1285        // Just exercise the BTF walk; a correctly built running kernel reports
1286        // no malformed kfuncs, but we don't assert emptiness since the test may
1287        // run on an affected kernel.
1288        let bad = super::malformed_scx_kfuncs();
1289        assert!(bad.iter().all(|n| !n.ends_with("_impl")));
1290    }
1291}