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::{anyhow, bail, Context, Result};
7use libbpf_rs::libbpf_sys::*;
8use libbpf_rs::{AsRawLibbpf, OpenProgramImpl, ProgramImpl};
9use log::{error, warn};
10use std::env;
11use std::ffi::c_void;
12use std::ffi::CStr;
13use std::ffi::CString;
14use std::io;
15use std::io::BufRead;
16use std::io::BufReader;
17use std::mem::size_of;
18use std::slice::from_raw_parts;
19
20const PROCFS_MOUNTS: &str = "/proc/mounts";
21const TRACEFS: &str = "tracefs";
22const DEBUGFS: &str = "debugfs";
23
24mod enums_abi {
25    include!("enums_abi.autogen.rs");
26}
27
28lazy_static::lazy_static! {
29    pub static ref SCX_OPS_KEEP_BUILTIN_IDLE: u64 =
30        read_enum("scx_ops_flags", "SCX_OPS_KEEP_BUILTIN_IDLE").unwrap_or(0);
31    pub static ref SCX_OPS_ENQ_LAST: u64 =
32        read_enum("scx_ops_flags", "SCX_OPS_ENQ_LAST").unwrap_or(0);
33    pub static ref SCX_OPS_ENQ_EXITING: u64 =
34        read_enum("scx_ops_flags", "SCX_OPS_ENQ_EXITING").unwrap_or(0);
35    pub static ref SCX_OPS_SWITCH_PARTIAL: u64 =
36        read_enum("scx_ops_flags", "SCX_OPS_SWITCH_PARTIAL").unwrap_or(0);
37    pub static ref SCX_OPS_ENQ_MIGRATION_DISABLED: u64 =
38        read_enum("scx_ops_flags", "SCX_OPS_ENQ_MIGRATION_DISABLED").unwrap_or(0);
39    pub static ref SCX_OPS_ALLOW_QUEUED_WAKEUP: u64 =
40        read_enum("scx_ops_flags", "SCX_OPS_ALLOW_QUEUED_WAKEUP").unwrap_or(0);
41    pub static ref SCX_OPS_BUILTIN_IDLE_PER_NODE: u64 =
42        read_enum("scx_ops_flags", "SCX_OPS_BUILTIN_IDLE_PER_NODE").unwrap_or(0);
43    pub static ref SCX_OPS_ALWAYS_ENQ_IMMED: u64 =
44        read_enum("scx_ops_flags", "SCX_OPS_ALWAYS_ENQ_IMMED").unwrap_or(0);
45
46    pub static ref SCX_PICK_IDLE_CORE: u64 =
47        read_enum("scx_pick_idle_cpu_flags", "SCX_PICK_IDLE_CORE").unwrap_or(0);
48    pub static ref SCX_PICK_IDLE_IN_NODE: u64 =
49        read_enum("scx_pick_idle_cpu_flags", "SCX_PICK_IDLE_IN_NODE").unwrap_or(0);
50
51    pub static ref ROOT_PREFIX: String =
52        env::var("SCX_SYSFS_PREFIX").unwrap_or("".to_string());
53}
54
55fn load_vmlinux_btf() -> &'static mut btf {
56    let btf = unsafe { btf__load_vmlinux_btf() };
57    if btf.is_null() {
58        panic!("btf__load_vmlinux_btf() returned NULL, was CONFIG_DEBUG_INFO_BTF enabled?")
59    }
60    unsafe { &mut *btf }
61}
62
63lazy_static::lazy_static! {
64    static ref VMLINUX_BTF: &'static mut btf = load_vmlinux_btf();
65}
66
67fn btf_kind(t: &btf_type) -> u32 {
68    (t.info >> 24) & 0x1f
69}
70
71fn btf_vlen(t: &btf_type) -> u32 {
72    t.info & 0xffff
73}
74
75fn btf_type_plus_1(t: &btf_type) -> *const c_void {
76    let ptr_val = t as *const btf_type as usize;
77    (ptr_val + size_of::<btf_type>()) as *const c_void
78}
79
80fn btf_enum(t: &btf_type) -> &[btf_enum] {
81    let ptr = btf_type_plus_1(t);
82    unsafe { from_raw_parts(ptr as *const btf_enum, btf_vlen(t) as usize) }
83}
84
85fn btf_enum64(t: &btf_type) -> &[btf_enum64] {
86    let ptr = btf_type_plus_1(t);
87    unsafe { from_raw_parts(ptr as *const btf_enum64, btf_vlen(t) as usize) }
88}
89
90fn btf_members(t: &btf_type) -> &[btf_member] {
91    let ptr = btf_type_plus_1(t);
92    unsafe { from_raw_parts(ptr as *const btf_member, btf_vlen(t) as usize) }
93}
94
95fn btf_params(t: &btf_type) -> &[btf_param] {
96    let ptr = btf_type_plus_1(t);
97    unsafe { from_raw_parts(ptr as *const btf_param, btf_vlen(t) as usize) }
98}
99
100fn btf_name_str_by_offset(btf: &btf, name_off: u32) -> Result<&str> {
101    let n = unsafe { btf__name_by_offset(btf, name_off) };
102    if n.is_null() {
103        bail!("btf__name_by_offset() returned NULL");
104    }
105    Ok(unsafe { CStr::from_ptr(n) }
106        .to_str()
107        .with_context(|| format!("Failed to convert {:?} to string", n))?)
108}
109
110/// Recover the true value of a 64-bit enum enumerator whose kernel BTF entry
111/// was truncated to its low 32 bits.
112///
113/// Kernels whose BTF was generated without BTF_KIND_ENUM64 support encode
114/// 64-bit enums as 8-byte BTF_KIND_ENUM entries whose enumerator values only
115/// carry the low 32 bits. This happens with pahole < 1.24, which predates
116/// ENUM64, and with pahole passing --skip_encoding_btf_enum64 (e.g. Google's
117/// Container-Optimized OS / GKE kernels deliberately pass it for backward
118/// compatibility with older BTF consumers). The high bits
119/// can't be recovered from kernel BTF, so substitute the value from the
120/// vmlinux.h this tree was built against, cross-checked against the low 32
121/// bits the kernel did provide.
122///
123/// Note that this is a best-effort recovery, not a ground truth. The
124/// substitution assumes the running kernel agrees with this tree's vmlinux.h
125/// on the high 32 bits, but only the low 32 bits can actually be verified.
126/// The cross-check is vacuous for enumerators whose value has no low bits
127/// set (e.g. SCX_DSQ_FLAG_BUILTIN, __SCX_ENQ_INTERNAL_MASK,
128/// SCX_ENQ_CLEAR_OPSS, SCX_ECODE_*): their lo32 is 0 and matches anything,
129/// so those substitutions rest entirely on the high bits never moving. An
130/// enumerator missing from the table (a kernel newer than this tree's
131/// vmlinux.h, or a stale autogen table) can't be recovered at all. If a
132/// substitution is ever wrong, the scheduler operates on bogus values (e.g.
133/// dispatching to nonexistent DSQ ids or silently dropping flags) and can
134/// wildly malfunction, which is why the mismatch and table-miss paths refuse
135/// instead of guessing.
136fn recover_truncated_enum64_from(
137    table: &[(&str, &str, u64)],
138    type_name: &str,
139    name: &str,
140    lo32: u32,
141) -> Result<u64> {
142    static WARN_ONCE: std::sync::Once = std::sync::Once::new();
143
144    let Some(&(_, _, abi_val)) = table.iter().find(|(t, n, _)| *t == type_name && *n == name)
145    else {
146        // Unknown enumerator (likely a stale autogen table). Fail
147        // pessimistically to avoid returning an invalid value. Log too, as
148        // callers commonly swallow the error with .unwrap_or(0).
149        let msg = format!(
150            "kernel BTF truncates 64-bit enum {}::{} to 0x{:x}; 64-bit \
151             variant not found in vmlinux.h",
152            type_name, name, lo32
153        );
154        error!("{}", msg);
155        bail!(msg);
156    };
157
158    if abi_val <= u32::MAX as u64 {
159        return Ok(lo32 as u64);
160    }
161
162    if abi_val as u32 != lo32 {
163        // Log too, as callers commonly swallow the error with .unwrap_or(0).
164        let msg = format!(
165            "kernel BTF value of {}::{} (0x{:x}) doesn't match the low 32 bits \
166             of the vmlinux.h value (0x{:x}); refusing to substitute",
167            type_name, name, lo32, abi_val
168        );
169        error!("{}", msg);
170        bail!(msg);
171    }
172
173    WARN_ONCE.call_once(|| {
174        warn!(
175            "kernel BTF lacks BTF_KIND_ENUM64 encoding (generated by \
176             pahole < 1.24 or with --skip_encoding_btf_enum64), so 64-bit \
177             scx enum values are truncated to their low 32 bits in kernel \
178             BTF. Substituting the full 64-bit values from the vmlinux.h \
179             this binary was built against, cross-checked against the low \
180             32 bits the kernel does provide. The high 32 bits cannot be \
181             verified: if the running kernel's actual values differ from \
182             the build-time vmlinux.h (e.g. an enum that moved in a newer \
183             kernel), the scheduler will operate on bogus values, such as \
184             dispatching to nonexistent DSQ ids, and can wildly malfunction."
185        );
186    });
187    Ok(abi_val)
188}
189
190fn recover_truncated_enum64(type_name: &str, name: &str, lo32: u32) -> Result<u64> {
191    recover_truncated_enum64_from(enums_abi::ENUM_ABI_VALUES, type_name, name, lo32)
192}
193
194pub fn read_enum(type_name: &str, name: &str) -> Result<u64> {
195    let btf: &btf = *VMLINUX_BTF;
196
197    let c_type_name = CString::new(type_name).unwrap();
198    let tid = unsafe { btf__find_by_name(btf, c_type_name.as_ptr()) };
199    if tid < 0 {
200        bail!("type {:?} doesn't exist, ret={}", type_name, tid);
201    }
202
203    let t = unsafe { btf__type_by_id(btf, tid as _) };
204    if t.is_null() {
205        bail!("btf__type_by_id({}) returned NULL", tid);
206    }
207    let t = unsafe { &*t };
208
209    match btf_kind(t) {
210        BTF_KIND_ENUM => {
211            for e in btf_enum(t).iter() {
212                if btf_name_str_by_offset(btf, e.name_off)? == name {
213                    // Try to recover a 64-bit enum from an 8-byte BTF_KIND_ENUM that was encoded
214                    // without ENUM64 support (old pahole or --skip_encoding_btf_enum64). Only
215                    // scx_* types are covered by the substitution table; non-scx types fall
216                    // through to the raw value so this generic utility keeps working for them.
217                    if unsafe { t.__bindgen_anon_1.size } == 8 && type_name.starts_with("scx_") {
218                        return recover_truncated_enum64(type_name, name, e.val as u32);
219                    }
220                    return Ok(e.val as u64);
221                }
222            }
223        }
224        BTF_KIND_ENUM64 => {
225            for e in btf_enum64(t).iter() {
226                if btf_name_str_by_offset(btf, e.name_off)? == name {
227                    return Ok(((e.val_hi32 as u64) << 32) | (e.val_lo32) as u64);
228                }
229            }
230        }
231        _ => (),
232    }
233
234    Err(anyhow!("{:?} doesn't exist in {:?}", name, type_name))
235}
236
237/// Read an enum value from the first BTF enum type that contains it.
238pub fn read_enum_any(type_names: &[&str], name: &str) -> Result<u64> {
239    let mut errors = Vec::new();
240
241    for type_name in type_names {
242        match read_enum(type_name, name) {
243            Ok(val) => return Ok(val),
244            Err(err) => errors.push(format!("{}: {:#}", type_name, err)),
245        }
246    }
247
248    bail!(
249        "{:?} doesn't exist in any of {:?}: {}",
250        name,
251        type_names,
252        errors.join("; ")
253    )
254}
255
256pub fn struct_has_field(type_name: &str, field: &str) -> Result<bool> {
257    let btf: &btf = *VMLINUX_BTF;
258
259    let c_type_name = CString::new(type_name).unwrap();
260    let tid = unsafe { btf__find_by_name_kind(btf, c_type_name.as_ptr(), BTF_KIND_STRUCT) };
261    if tid < 0 {
262        bail!("type {:?} doesn't exist, ret={}", type_name, tid);
263    }
264
265    let t = unsafe { btf__type_by_id(btf, tid as _) };
266    if t.is_null() {
267        bail!("btf__type_by_id({}) returned NULL", tid);
268    }
269    let t = unsafe { &*t };
270
271    for m in btf_members(t).iter() {
272        if btf_name_str_by_offset(btf, m.name_off)? == field {
273            return Ok(true);
274        }
275    }
276
277    Ok(false)
278}
279
280pub fn ksym_exists(ksym: &str) -> Result<bool> {
281    let btf: &btf = *VMLINUX_BTF;
282
283    let ksym_name = CString::new(ksym).unwrap();
284    let tid = unsafe { btf__find_by_name(btf, ksym_name.as_ptr()) };
285    Ok(tid >= 0)
286}
287
288/// Scan the running kernel's vmlinux BTF for scx kfuncs whose public-facing
289/// prototype still carries the implicit `aux` (struct bpf_prog_aux *) argument.
290///
291/// This is the KF_IMPLICIT_ARGS-on-pahole-<1.26 bug: pahole < 1.26 fails to
292/// split such kfuncs into a public prototype (without `aux`) and an `_impl`
293/// variant (with it), so the visible prototype keeps `aux`. BPF programs that
294/// declare the kfunc without it then fail to load with the confusing
295/// 'func_proto incompatible with vmlinux' error. Returns the names of the
296/// affected kfuncs, so a clear diagnostic can be produced on load failure.
297pub fn malformed_scx_kfuncs() -> Vec<String> {
298    let btf: &btf = *VMLINUX_BTF;
299    let mut bad = Vec::new();
300    let cnt = unsafe { btf__type_cnt(btf) };
301
302    for id in 1..cnt {
303        let t = unsafe { btf__type_by_id(btf, id) };
304        if t.is_null() {
305            continue;
306        }
307        let t = unsafe { &*t };
308        if btf_kind(t) != BTF_KIND_FUNC {
309            continue;
310        }
311
312        let Ok(name) = btf_name_str_by_offset(btf, t.name_off) else {
313            continue;
314        };
315        if !(name.starts_with("scx_bpf_") || name.starts_with("__scx_bpf_")) {
316            continue;
317        }
318        // The implicit `aux` argument legitimately appears on the `_impl`
319        // variant that resolve_btfids splits off; only the public-facing name
320        // (without the `_impl` suffix) should be free of it. On a broken kernel
321        // the split never happens, so there is no `_impl` variant and the
322        // public name itself keeps `aux`.
323        if name.ends_with("_impl") {
324            continue;
325        }
326
327        // FUNC -> FUNC_PROTO
328        let proto_id = unsafe { t.__bindgen_anon_1.type_ };
329        let pt = unsafe { btf__type_by_id(btf, proto_id) };
330        if pt.is_null() {
331            continue;
332        }
333        let pt = unsafe { &*pt };
334        if btf_kind(pt) != BTF_KIND_FUNC_PROTO {
335            continue;
336        }
337
338        if btf_params(pt).iter().any(|p| {
339            p.name_off != 0 && matches!(btf_name_str_by_offset(btf, p.name_off), Ok("aux"))
340        }) {
341            bad.push(name.to_string());
342        }
343    }
344
345    bad
346}
347
348pub fn in_kallsyms(ksym: &str) -> Result<bool> {
349    let file = std::fs::File::open("/proc/kallsyms")?;
350    let reader = std::io::BufReader::new(file);
351
352    for line in reader.lines() {
353        for sym in line.unwrap().split_whitespace() {
354            if ksym == sym {
355                return Ok(true);
356            }
357        }
358    }
359
360    Ok(false)
361}
362
363/// Returns the mount point for a filesystem type.
364pub fn get_fs_mount(mount_type: &str) -> Result<Vec<std::path::PathBuf>> {
365    let proc_mounts_path = std::path::Path::new(PROCFS_MOUNTS);
366
367    let file = std::fs::File::open(proc_mounts_path)
368        .with_context(|| format!("Failed to open {}", proc_mounts_path.display()))?;
369
370    let reader = BufReader::new(file);
371
372    let mut mounts = Vec::new();
373    for line in reader.lines() {
374        let line = line.context("Failed to read line from /proc/mounts")?;
375        let mount_info: Vec<&str> = line.split_whitespace().collect();
376
377        if mount_info.len() > 3 && mount_info[2] == mount_type {
378            let mount_path = std::path::PathBuf::from(mount_info[1]);
379            mounts.push(mount_path);
380        }
381    }
382
383    Ok(mounts)
384}
385
386/// Returns the tracefs mount point.
387pub fn tracefs_mount() -> Result<std::path::PathBuf> {
388    let mounts = get_fs_mount(TRACEFS)?;
389    mounts.into_iter().next().context("No tracefs mount found")
390}
391
392/// Returns the debugfs mount point.
393pub fn debugfs_mount() -> Result<std::path::PathBuf> {
394    let mounts = get_fs_mount(DEBUGFS)?;
395    mounts.into_iter().next().context("No debugfs mount found")
396}
397
398pub fn tracer_available(tracer: &str) -> Result<bool> {
399    let base_path = tracefs_mount().unwrap_or_else(|_| debugfs_mount().unwrap().join("tracing"));
400    let file = match std::fs::File::open(base_path.join("available_tracers")) {
401        Ok(f) => f,
402        Err(_) => return Ok(false),
403    };
404    let reader = std::io::BufReader::new(file);
405
406    for line in reader.lines() {
407        for tc in line.unwrap().split_whitespace() {
408            if tracer == tc {
409                return Ok(true);
410            }
411        }
412    }
413
414    Ok(false)
415}
416
417pub fn tracepoint_exists(tracepoint: &str) -> Result<bool> {
418    let base_path = tracefs_mount().unwrap_or_else(|_| debugfs_mount().unwrap().join("tracing"));
419    let file = match std::fs::File::open(base_path.join("available_events")) {
420        Ok(f) => f,
421        Err(_) => return Ok(false),
422    };
423    let reader = std::io::BufReader::new(file);
424
425    for line in reader.lines() {
426        for tp in line.unwrap().split_whitespace() {
427            if tracepoint == tp {
428                return Ok(true);
429            }
430        }
431    }
432
433    Ok(false)
434}
435
436pub fn cond_kprobe_enable<T>(sym: &str, prog_ptr: &OpenProgramImpl<T>) -> Result<bool> {
437    if in_kallsyms(sym)? {
438        unsafe {
439            bpf_program__set_autoload(prog_ptr.as_libbpf_object().as_ptr(), true);
440        }
441        return Ok(true);
442    } else {
443        warn!("symbol {sym} is missing, kprobe not loaded");
444    }
445
446    Ok(false)
447}
448
449pub fn cond_kprobes_enable<T>(kprobes: Vec<(&str, &OpenProgramImpl<T>)>) -> Result<bool> {
450    // Check if all the symbols exist.
451    for (sym, _) in kprobes.iter() {
452        if in_kallsyms(sym)? == false {
453            warn!("symbol {sym} is missing, kprobe not loaded");
454            return Ok(false);
455        }
456    }
457
458    // Enable all the tracepoints.
459    for (_, ptr) in kprobes.iter() {
460        unsafe {
461            bpf_program__set_autoload(ptr.as_libbpf_object().as_ptr(), true);
462        }
463    }
464
465    Ok(true)
466}
467
468pub fn cond_kprobe_load<T>(sym: &str, prog_ptr: &OpenProgramImpl<T>) -> Result<bool> {
469    if in_kallsyms(sym)? {
470        unsafe {
471            bpf_program__set_autoload(prog_ptr.as_libbpf_object().as_ptr(), true);
472            bpf_program__set_autoattach(prog_ptr.as_libbpf_object().as_ptr(), false);
473        }
474        return Ok(true);
475    } else {
476        warn!("symbol {sym} is missing, kprobe not loaded");
477    }
478
479    Ok(false)
480}
481
482pub fn cond_kprobe_attach<T>(sym: &str, prog_ptr: &ProgramImpl<T>) -> Result<bool> {
483    if in_kallsyms(sym)? {
484        unsafe {
485            bpf_program__attach(prog_ptr.as_libbpf_object().as_ptr());
486        }
487        return Ok(true);
488    } else {
489        warn!("symbol {sym} is missing, kprobe not loaded");
490    }
491
492    Ok(false)
493}
494
495pub fn cond_tracepoint_enable<T>(tracepoint: &str, prog_ptr: &OpenProgramImpl<T>) -> Result<bool> {
496    if tracepoint_exists(tracepoint)? {
497        unsafe {
498            bpf_program__set_autoload(prog_ptr.as_libbpf_object().as_ptr(), true);
499        }
500        return Ok(true);
501    } else {
502        warn!("tracepoint {tracepoint} is missing, tracepoint not loaded");
503    }
504
505    Ok(false)
506}
507
508pub fn cond_tracepoints_enable<T>(tracepoints: Vec<(&str, &OpenProgramImpl<T>)>) -> Result<bool> {
509    // Check if all the tracepoints exist.
510    for (tp, _) in tracepoints.iter() {
511        if tracepoint_exists(tp)? == false {
512            warn!("tracepoint {tp} is missing, tracepoint not loaded");
513            return Ok(false);
514        }
515    }
516
517    // Enable all the tracepoints.
518    for (_, ptr) in tracepoints.iter() {
519        unsafe {
520            bpf_program__set_autoload(ptr.as_libbpf_object().as_ptr(), true);
521        }
522    }
523
524    Ok(true)
525}
526
527pub fn is_sched_ext_enabled() -> io::Result<bool> {
528    let content = std::fs::read_to_string("/sys/kernel/sched_ext/state")?;
529
530    match content.trim() {
531        "enabled" => Ok(true),
532        "disabled" => Ok(false),
533        _ => {
534            // Error if the content is neither "enabled" nor "disabled"
535            Err(io::Error::new(
536                io::ErrorKind::InvalidData,
537                "Unexpected content in /sys/kernel/sched_ext/state",
538            ))
539        }
540    }
541}
542
543#[macro_export]
544macro_rules! unwrap_or_break {
545    ($expr: expr, $label: lifetime) => {{
546        match $expr {
547            Ok(val) => val,
548            Err(e) => break $label Err(e),
549        }
550    }};
551}
552
553pub fn check_min_requirements() -> Result<()> {
554    // ec7e3b0463e1 ("implement-ops") in https://github.com/sched-ext/sched_ext
555    // is the current minimum required kernel version.
556    if let Ok(false) | Err(_) = struct_has_field("sched_ext_ops", "dump") {
557        bail!("sched_ext_ops.dump() missing, kernel too old?");
558    }
559    Ok(())
560}
561
562/// struct sched_ext_ops can change over time. If compat.bpf.h::SCX_OPS_DEFINE()
563/// is used to define ops, and scx_ops_open!(), scx_ops_load!(), and
564/// scx_ops_attach!() are used to open, load and attach it, backward
565/// compatibility is automatically maintained where reasonable.
566#[rustfmt::skip]
567#[macro_export]
568macro_rules! scx_ops_open {
569    ($builder: expr, $obj_ref: expr, $ops: ident, $open_opts: expr) => { 'block: {
570        scx_utils::paste! {
571        scx_utils::unwrap_or_break!(scx_utils::compat::check_min_requirements(), 'block);
572            use ::anyhow::Context;
573            use ::libbpf_rs::skel::SkelBuilder;
574
575            let mut skel = match $open_opts {
576                Some(opts_ref) => { // Match a reference directly
577                    match $builder.open_opts(opts_ref, $obj_ref).context("Failed to open BPF program with options") {
578                        Ok(val) => val,
579                        Err(e) => break 'block Err(e),
580                    }
581                }
582                None => {
583                    match $builder.open($obj_ref).context("Failed to open BPF program") {
584                        Ok(val) => val,
585                        Err(e) => break 'block Err(e),
586                    }
587                }
588            };
589
590            let ops = skel.struct_ops.[<$ops _mut>]();
591            let path = std::path::Path::new("/sys/kernel/sched_ext/hotplug_seq");
592
593            let val = match std::fs::read_to_string(&path) {
594                Ok(val) => val,
595                Err(_) => {
596                    break 'block Err(anyhow::anyhow!("Failed to open or read file {:?}", path));
597                }
598            };
599
600            ops.hotplug_seq = match val.trim().parse::<u64>() {
601                Ok(parsed) => parsed,
602                Err(_) => {
603                    break 'block Err(anyhow::anyhow!("Failed to parse hotplug seq {}", val));
604                }
605            };
606
607            if let Ok(s) = ::std::env::var("SCX_TIMEOUT_MS") {
608                skel.struct_ops.[<$ops _mut>]().timeout_ms = match s.parse::<u32>() {
609                    Ok(ms) => {
610                        ::scx_utils::info!("Setting timeout_ms to {} based on environment", ms);
611                        ms
612                    },
613                    Err(e) => {
614                        break 'block anyhow::Result::Err(e).context("SCX_TIMEOUT_MS has invalid value");
615                    },
616                };
617            }
618
619            {
620                let ops = skel.struct_ops.[<$ops _mut>]();
621
622                let name_field = &mut ops.name;
623
624                let version_suffix = ::scx_utils::build_id::ops_version_suffix(env!("CARGO_PKG_VERSION"));
625                let bytes = version_suffix.as_bytes();
626                let mut i = 0;
627                let mut bytes_idx = 0;
628                let mut found_null = false;
629
630                while i < name_field.len() - 1 {
631                    found_null |= name_field[i] == 0;
632                    if !found_null {
633                        i += 1;
634                        continue;
635                    }
636
637                    if bytes_idx < bytes.len() {
638                        name_field[i] = bytes[bytes_idx] as i8;
639                        bytes_idx += 1;
640                    } else {
641                        break;
642                    }
643                    i += 1;
644                }
645                name_field[i] = 0;
646            }
647
648            $crate::import_enums!(skel);
649
650            let result = ::anyhow::Result::Ok(skel);
651
652            result
653        }
654    }};
655}
656
657/// struct sched_ext_ops can change over time. If compat.bpf.h::SCX_OPS_DEFINE()
658/// is used to define ops, and scx_ops_open!(), scx_ops_load!(), and
659/// scx_ops_attach!() are used to open, load and attach it, backward
660/// compatibility is automatically maintained where reasonable.
661#[rustfmt::skip]
662#[macro_export]
663macro_rules! scx_ops_load {
664    ($skel: expr, $ops: ident, $uei: ident) => { 'block: {
665        scx_utils::paste! {
666            use ::anyhow::Context;
667            use ::libbpf_rs::skel::OpenSkel;
668
669            {
670                let ops = $skel.struct_ops.[<$ops _mut>]();
671                if ops.sub_cgroup_id > 0 {
672                    if let Ok(false) | Err(_) = scx_utils::compat::struct_has_field("sched_ext_ops", "sub_cgroup_id") {
673                        ::scx_utils::warn!("kernel doesn't support ops.sub_cgroup_id");
674                        ops.sub_cgroup_id = 0;
675                    }
676                }
677            }
678
679            scx_utils::uei_set_size!($skel, $ops, $uei);
680            $skel.load().context("Failed to load BPF program").map_err(|e| {
681                let bad = scx_utils::compat::malformed_scx_kfuncs();
682                if bad.is_empty() {
683                    e
684                } else {
685                    e.context(format!(
686                        "the running kernel's BTF has malformed scx kfunc prototype(s): {}.\n\
687                         \n\
688                         These kfuncs are KF_IMPLICIT_ARGS but their public BTF prototype\n\
689                         still carries the implicit 'struct bpf_prog_aux *' argument, which\n\
690                         makes BPF programs fail to load with 'func_proto incompatible with\n\
691                         vmlinux'. This happens when the kernel was built with pahole < 1.26.\n\
692                         \n\
693                         Fix: boot a kernel whose BTF was generated with pahole >= 1.26.\n\
694                         Affected distros include Ubuntu 24.04 LTS. See kernel commit\n\
695                         9edd04c4189e (\"docs: Raise minimum pahole version to 1.26 for\n\
696                         KF_IMPLICIT_ARGS kfuncs\").",
697                        bad.join(", ")
698                    ))
699                }
700            })
701        }
702    }};
703}
704
705/// Must be used together with scx_ops_load!(). See there.
706#[rustfmt::skip]
707#[macro_export]
708macro_rules! scx_ops_attach {
709    ($skel: expr, $ops: ident) => {
710        scx_ops_attach!($skel, $ops, false)
711    };
712    ($skel: expr, $ops: ident, $is_subsched: expr) => { 'block: {
713        use ::anyhow::Context;
714        use ::libbpf_rs::skel::Skel;
715
716        if !$is_subsched && scx_utils::compat::is_sched_ext_enabled().unwrap_or(false) {
717            break 'block Err(anyhow::anyhow!(
718                "another sched_ext scheduler is already running"
719            ));
720        }
721        $skel
722            .attach()
723            .context("Failed to attach non-struct_ops BPF programs")
724            .and_then(|_| {
725                $skel
726                    .maps
727                    .$ops
728                    .attach_struct_ops()
729                    .context("Failed to attach struct_ops BPF programs")
730            })
731    }};
732}
733
734#[cfg(test)]
735mod tests {
736    #[test]
737    fn test_read_enum() {
738        assert_eq!(super::read_enum("pid_type", "PIDTYPE_TGID").unwrap(), 1);
739    }
740
741    #[test]
742    fn test_read_enum_any() {
743        assert_eq!(
744            super::read_enum_any(&["NO_SUCH_TYPE", "pid_type"], "PIDTYPE_TGID").unwrap(),
745            1
746        );
747        assert!(super::read_enum_any(&["NO_SUCH_TYPE", "pid_type"], "NO_SUCH_ENUM").is_err());
748    }
749
750    #[test]
751    fn test_recover_truncated_enum64() {
752        let table: &[(&str, &str, u64)] = &[
753            ("scx_dsq_id_flags", "SCX_DSQ_LOCAL", 0x8000000000000002),
754            ("scx_enq_flags", "SCX_ENQ_PREEMPT", 0x100000000),
755            ("scx_enq_flags", "SCX_ENQ_HEAD", 0x10000),
756        ];
757
758        // >32-bit values with matching low bits get substituted.
759        assert_eq!(
760            super::recover_truncated_enum64_from(table, "scx_dsq_id_flags", "SCX_DSQ_LOCAL", 2)
761                .unwrap(),
762            0x8000000000000002
763        );
764        assert_eq!(
765            super::recover_truncated_enum64_from(table, "scx_enq_flags", "SCX_ENQ_PREEMPT", 0)
766                .unwrap(),
767            0x100000000
768        );
769        // Sub-32-bit values truncate losslessly, so the kernel's value stays
770        // authoritative even when it disagrees with the table.
771        assert_eq!(
772            super::recover_truncated_enum64_from(table, "scx_enq_flags", "SCX_ENQ_HEAD", 0x20000)
773                .unwrap(),
774            0x20000
775        );
776        // A low-32 mismatch on a >32-bit value is ABI drift; refuse.
777        assert!(super::recover_truncated_enum64_from(
778            table,
779            "scx_dsq_id_flags",
780            "SCX_DSQ_LOCAL",
781            3
782        )
783        .is_err());
784        // Unknown enumerators fail pessimistically (stale autogen table).
785        assert!(
786            super::recover_truncated_enum64_from(table, "scx_enq_flags", "SCX_ENQ_NEW", 7).is_err()
787        );
788    }
789
790    #[test]
791    fn test_enum_abi_table() {
792        // Spot-check the autogenerated table against ABI values that have
793        // been stable on every kernel that ships sched_ext.
794        let find = |t: &str, n: &str| {
795            super::enums_abi::ENUM_ABI_VALUES
796                .iter()
797                .find(|(ty, na, _)| *ty == t && *na == n)
798                .map(|&(_, _, v)| v)
799        };
800        assert_eq!(
801            find("scx_dsq_id_flags", "SCX_DSQ_FLAG_BUILTIN"),
802            Some(1 << 63)
803        );
804        assert_eq!(
805            find("scx_dsq_id_flags", "SCX_DSQ_LOCAL"),
806            Some((1 << 63) | 2)
807        );
808        assert_eq!(
809            find("scx_dsq_id_flags", "SCX_DSQ_LOCAL_ON"),
810            Some((1 << 63) | (1 << 62))
811        );
812        assert_eq!(find("scx_public_consts", "SCX_SLICE_INF"), Some(u64::MAX));
813        assert_eq!(find("scx_enq_flags", "SCX_ENQ_PREEMPT"), Some(1 << 32));
814    }
815
816    #[test]
817    fn test_struct_has_field() {
818        assert!(super::struct_has_field("task_struct", "flags").unwrap());
819        assert!(!super::struct_has_field("task_struct", "NO_SUCH_FIELD").unwrap());
820        assert!(super::struct_has_field("NO_SUCH_STRUCT", "NO_SUCH_FIELD").is_err());
821    }
822
823    #[test]
824    fn test_ksym_exists() {
825        assert!(super::ksym_exists("bpf_task_acquire").unwrap());
826        assert!(!super::ksym_exists("NO_SUCH_KFUNC").unwrap());
827    }
828
829    #[test]
830    fn test_malformed_scx_kfuncs() {
831        // Just exercise the BTF walk; a correctly built running kernel reports
832        // no malformed kfuncs, but we don't assert emptiness since the test may
833        // run on an affected kernel.
834        let bad = super::malformed_scx_kfuncs();
835        assert!(bad.iter().all(|n| !n.ends_with("_impl")));
836    }
837}