Skip to content

RFC: scx_utils, scx/compat: Recover enum64 values truncated by kernel BTF - #3708

Merged
htejun merged 1 commit into
mainfrom
upstream/enum64-btf-fallback
Jul 20, 2026
Merged

RFC: scx_utils, scx/compat: Recover enum64 values truncated by kernel BTF#3708
htejun merged 1 commit into
mainfrom
upstream/enum64-btf-fallback

Conversation

@ByteLab-David

@ByteLab-David ByteLab-David commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

TL;DR: Some >= 6.12 kernels in the wild don't compile with a sufficiently modern pahole to get 64-bit enums in BTF. This is a proposed hacky workaround to generate u64s from vmlinux.h rather than relying on runtime BTF

This is admittedly kind of shitty and hacky, but practically speaking I think it should work fine, and the alternative is that shit just won't work on those platforms at all until they compile with the correct pahole. The worst part of this change is probably the logic added to gen_enum_defs.py. It's not great, but OTOH it should be fine and presumably isn't going to change or break as long as bpftool doesn't change how it emits vmlinux.h.

Here's the PR summary:

Kernels whose BTF was generated without BTF_KIND_ENUM64 support encode 64-bit enums as 8-byte BTF_KIND_ENUM entries whose enumerator values are truncated to the low 32 bits. This happens with pahole < 1.24, which predates ENUM64 (and with newer pahole passing
--skip_encoding_btf_enum64). Google's Container-Optimized OS (the GKE kernel) uses an older version of pahole, so it functionally can't be used with sched_ext even on >= 6.12 kernels.

On such kernels the runtime enum import reads SCX_DSQ_LOCAL as 0x2 instead of 0x8000000000000002, so every dispatch to a builtin DSQ targets a bogus user DSQ id and the kernel ejects the scheduler with "non-existent DSQ 0x2" (or "0x" for LOCAL_ON dispatches) right after attach. SCX_ENQ_PREEMPT/REENQ/LAST also read as 0, silently dropping preempt semantics. Observed on GKE COS-129 nodes (kernel 6.12.77+):

  ENUM 'scx_dsq_id_flags' encoding=UNSIGNED size=8 vlen=7
          'SCX_DSQ_FLAG_BUILTIN' val=0
          'SCX_DSQ_LOCAL' val=2
          ...

Let's work around this by detecting the truncation structurally in the enum readers: an 8-byte BTF_KIND_ENUM can only be a downgraded 64-bit enum, since any encoder with enum64 support would emit BTF_KIND_ENUM64 for those. When one is found, substitute the value from the vmlinux.h the tree is built against, cross-checked against the low 32 bits the kernel did provide:

  • If the vmlinux.h value fits in 32 bits, the truncation was lossless and the kernel's value stands as-is. This keeps enumerators that have moved across kernel versions (e.g. SCX_ENQ_HEAD) working.

  • If it exceeds 32 bits and the low bits match, substitute it and warn once.

  • If the low bits don't match, refuse, as the ABI has drifted.

The substitution tables are generated from vmlinux.h by scripts/gen_enum_defs.py (which previously only emitted the HAVE_* defines) into scheds/include/scx/enums_abi.autogen.h and rust/scx_utils/src/enums_abi.autogen.rs, so there's no hand-maintained value list to keep in sync and newly added 64-bit enums are covered whenever vmlinux.h and the tables are regenerated. Because the recovery lives in COMPAT_read_enum() / compat::read_enum(), it covers the SCX_OPS* and SCX_PICK_IDLE* reads and read_enum_any() too, not just the SCX_ENUM_INIT() import path.

Regenerating also caught a stale enum_defs.autogen.h: vmlinux.h already has SCX_ENQ_GDSQ_FALLBACK but its HAVE_ define was missing.

Verified with the scx_utils unit tests and a small userspace harness poking __COMPAT_recover_truncated_enum64() directly (substitution, lossless, and mismatch cases).

@ByteLab-David

Copy link
Copy Markdown
Contributor Author

By the way, I checked w/ Google and this isn't an old pahole issue -- they're deliberately passing --skip_encoding_btf_enum64 for BC reasons. So without this, there is no way to use sched_ext on GKE kernels.

@likewhatevs

Copy link
Copy Markdown
Contributor

I'm going to add a GKE kernel to here (still debugging misc qol issues w/ it): #3700

ATM I'm tracking 3 recent compat issues:

  1. dwarves 1.26 break -- we have a nice err msg for this but I have not seen this be an issue.
  2. dwarves 1.31 compat -- this seems to be required, using 1.30 as fedora does triggers 1.26 err msg.
  3. this pr -- idk, I'm gonna add gke+layered to the list of kernels+schedulers tested also.

@ByteLab-David

Copy link
Copy Markdown
Contributor Author

I'm going to add a GKE kernel to here (still debugging misc qol issues w/ it): #3700

ATM I'm tracking 3 recent compat issues:

  1. dwarves 1.26 break -- we have a nice err msg for this but I have not seen this be an issue.
  2. dwarves 1.31 compat -- this seems to be required, using 1.30 as fedora does triggers 1.26 err msg.
  3. this pr -- idk, I'm gonna add gke+layered to the list of kernels+schedulers tested also.

Nice, yeah that would be cool. If you don't mind, could you please also add one for EKS? Basically any cloud provider running k8s that's on a sufficiently modern kernel would be good to get coverage on.

@likewhatevs

Copy link
Copy Markdown
Contributor

Nice, yeah that would be cool. If you don't mind, could you please also add one for EKS? Basically any cloud provider running k8s that's on a sufficiently modern kernel would be good to get coverage on.

Yeah so far I have steamos, fedora, amazonlinux, ubuntu, gke. I think that covers everything (although I was totally unaware of that gke issue earlier).

@htejun htejun left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welcome back @ByteLab-David! Generally looks useful to me but I think it'd help to complain louder.

Comment thread rust/scx_utils/src/compat.rs Outdated
WARN_ONCE.call_once(|| {
warn!(
"kernel BTF lacks ENUM64 encoding; substituting vmlinux.h values \
for truncated 64-bit scx enums"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be useful to explain the situation in more detail pointing to what may be missing and that this carries risk of wildly malfunctioning.

@htejun

htejun commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Claude-generated review. Suggestions:

  1. Log in the Rust failure paths. All in-tree callers do read_enum(...).unwrap_or(0) and log::warn! only fires on success, so both refuse cases (low-32 mismatch, table miss) silently degrade to 0, the same failure mode being fixed. The C side prints to stderr; do the equivalent in recover_truncated_enum64_from() before bailing.

  2. Note in the comment that the low-32 cross-check is vacuous for pure-high-bit flags (SCX_DSQ_FLAG_BUILTIN, __SCX_ENQ_INTERNAL_MASK, SCX_ENQ_CLEAR_OPSS, SCX_ECODE_*, ...): lo32 == 0 always matches, so those rest entirely on the high bits never moving.

  3. Restrict the pessimistic table-miss failure to scx_-prefixed type names and fall through to the raw value otherwise. read_enum() / __COMPAT_read_enum() are generic public utilities and the table can never help non-scx types.

  4. Add a CI check that reruns gen_enum_defs.py against the in-tree vmlinux.h and fails on diff, and make the vmlinux.h update procedure pass the two new arguments. The stale HAVE_SCX_ENQ_GDSQ_FALLBACK shows manual regeneration gets skipped, and a stale table turns the miss-failure path into the common case.

  5. Add a small in-tree test for __COMPAT_recover_truncated_enum64(); the Rust twin has coverage and the two must stay behaviorally identical.

Kernels whose BTF was generated without BTF_KIND_ENUM64 support encode
64-bit enums as 8-byte BTF_KIND_ENUM entries whose enumerator values are
truncated to the low 32 bits. This happens with pahole < 1.24, which
predates ENUM64, and with pahole passing --skip_encoding_btf_enum64.
Google's Container-Optimized OS (the GKE kernel) deliberately passes
--skip_encoding_btf_enum64 for backward compatibility with older BTF
consumers, so sched_ext functionally can't be used on GKE even on
>= 6.12 kernels.

On such kernels the runtime enum import reads SCX_DSQ_LOCAL as 0x2
instead of 0x8000000000000002, so every dispatch to a builtin DSQ
targets a bogus user DSQ id and the kernel ejects the scheduler with
"non-existent DSQ 0x2" (or "0x<cpu>" for LOCAL_ON dispatches) right
after attach. SCX_ENQ_PREEMPT/REENQ/LAST also read as 0, silently
dropping preempt semantics. Observed on GKE COS-129 nodes (kernel
6.12.77+):

```
  ENUM 'scx_dsq_id_flags' encoding=UNSIGNED size=8 vlen=7
          'SCX_DSQ_FLAG_BUILTIN' val=0
          'SCX_DSQ_LOCAL' val=2
          ...
```

Let's work around this by detecting the truncation structurally in the
enum readers: an 8-byte BTF_KIND_ENUM can only be a downgraded 64-bit
enum, since any encoder with enum64 support would emit BTF_KIND_ENUM64
for those. When one is found, substitute the value from the vmlinux.h
the tree is built against, cross-checked against the low 32 bits the
kernel did provide:

- If the vmlinux.h value fits in 32 bits, the truncation was lossless and
  the kernel's value stands as-is. This keeps enumerators that have moved
  across kernel versions (e.g. SCX_ENQ_HEAD) working.

- If it exceeds 32 bits and the low bits match, substitute it and warn
  once. The cross-check is vacuous for enumerators with no low bits set
  (SCX_DSQ_FLAG_BUILTIN and friends), so the warning spells out exactly
  what is being assumed and that a wrong substitution can make the
  scheduler wildly malfunction.

- If the low bits don't match, refuse, as the ABI has drifted.

The recovery only applies to scx_-prefixed enum types: the substitution
table can never help other types, and read_enum() / __COMPAT_read_enum()
are generic public utilities, so non-scx 8-byte enums keep returning the
raw kernel value. Both refuse paths (low-32 mismatch and table miss) log
the failure before erroring out, as in-tree callers commonly swallow the
error with unwrap_or(0).

The substitution tables are generated from vmlinux.h by
scripts/gen_enum_defs.py (which previously only emitted the HAVE_*
defines) into scheds/include/scx/enums_abi.autogen.h and
rust/scx_utils/src/enums_abi.autogen.rs, so there's no hand-maintained
value list to keep in sync and newly added 64-bit enums are covered
whenever vmlinux.h and the tables are regenerated. Because the recovery
lives in __COMPAT_read_enum() / compat::read_enum(), it covers the
SCX_OPS_* and SCX_PICK_IDLE_* reads and read_enum_any() too, not just
the SCX_ENUM_INIT() import path.

To keep the autogen files from going stale, gen_vmlinux_h.sh now
regenerates them on every vmlinux.h update, and CI fails if rerunning
gen_enum_defs.py against the in-tree vmlinux.h produces any diff.
Regenerating already caught a stale enum_defs.autogen.h: vmlinux.h has
SCX_ENQ_GDSQ_FALLBACK but its HAVE_ define was missing, showing that
manual regeneration gets skipped -- and a stale table here would turn
the table-miss refusal into the common case.

Verified with the scx_utils unit tests and a new in-tree userspace test,
lib/selftests/compat/test_enum64_recovery, which is wired into the CI
unit-test job, pokes __COMPAT_recover_truncated_enum64() directly, and
mirrors the Rust tests (substitution, lossless truncation, low-32
mismatch, and table-miss cases plus ABI table spot-checks); the C and
Rust implementations must stay behaviorally identical.

Signed-off-by: David Vernet <void@mainfault.com>
@ByteLab-David
ByteLab-David force-pushed the upstream/enum64-btf-fallback branch from 2ee188c to 596dcf4 Compare July 18, 2026 19:10
@ByteLab-David

Copy link
Copy Markdown
Contributor Author

Thanks for the review @htejun. Addressed all feedback.

@multics69 multics69 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welcome back @ByteLab-David ! Looks good to me.

@htejun
htejun added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit 9473c49 Jul 20, 2026
14 checks passed
@htejun
htejun deleted the upstream/enum64-btf-fallback branch July 20, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants