Skip to content

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

Open
ByteLab-David wants to merge 1 commit into
mainfrom
upstream/enum64-btf-fallback
Open

RFC: scx_utils, scx/compat: Recover enum64 values truncated by kernel BTF#3708
ByteLab-David wants to merge 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).

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<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.

- 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).

Signed-off-by: David Vernet <void@mainfault.com>
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.

1 participant