Skip to content

Commit 5d0b226

Browse files
Vmlinux gen fix (#97)
* widen bitfields whose declared width exceeds their base type clang2py can emit a bitfield typed as a smaller ctype than its own declared width (e.g. a 15-bit field typed ctypes.c_ubyte, which only has 8 bits) - ctypes rejects these with 'ValueError: number of bits invalid for bit field'. Surfaced by struct_vmbus_channel_offer_channel on a kernel with Hyper-V support enabled (not present locally, but present on GitHub Actions' Azure-hosted runners). Generalizes the existing c_bool-specific workaround to all integer ctypes, widening to the smallest standard type that fits. * fix ruff-format violation in vmlinux-gen.py * Add CI workflow to run the test suite (#96) * add CI workflow to run the test suite Runs make test on every push/PR. Kernel verifier tests need passwordless sudo and a kernel with BTF/BPF enabled, which isn't guaranteed on every runner, so the workflow probes for working sudo first and only attempts them if it's available. Also add ctypeslib2 to the test extra: make test now regenerates vmlinux.py via tools/vmlinux-gen.py, which needs clang2py. * fix CI: bpftool is a virtual package on Ubuntu, install linux-tools instead * fix CI: locate bpftool binary and add it to PATH manually linux-tools-generic's update-alternatives symlink for bpftool doesn't fire on GitHub-hosted runners, since their kernel version has no matching linux-tools-<version> package. * pin clang bindings to 16.0.6 to match Ubuntu's default libclang pip installs the latest 'clang' release by default, whose libclang API surface is newer than Ubuntu 24.04's apt libclang-16, causing a LibclangError about an undefined symbol. Older bindings against a newer libclang stay compatible, so pin the bindings low rather than the system library. * DEBUG: dump struct_vmbus_channel_offer_channel on test failure * remove debug step now that the bitfield issue is fixed * fix CI: install LLVM 19 from apt.llvm.org, Ubuntu's default is too old llvmlite>=0.49's ArgumentAttributes only recognizes the LLVM 19+ 'captures(none)' spelling of the renamed 'nocapture' attribute. Ubuntu noble's default llvm/clang packages are LLVM 18, whose llc can't parse that attribute in the emitted .ll text. * DEBUG: show real llc stderr on failure * fix YAML syntax in debug step * fix CI: install LLVM 22 (matching llvmlite's bundled version), not 19 Verified on CI: LLVM 19's llc still rejects 'captures(none)' as a parse error ('expected ) at end of argument list'). llvmlite 0.49 bundles LLVM 22.1.0 internally; match that generation instead. * remove debug step, LLVM 22 fix confirmed working on CI * avoid running CI twice per push: scope push trigger to master Branches live in this repo, not forks, so a push to a branch with an open PR fired both push and pull_request for the same commit. push now only fires for master (a post-merge check); pull_request already covers every commit on a feature branch.
1 parent 3c69ec3 commit 5d0b226

3 files changed

Lines changed: 150 additions & 7 deletions

File tree

.github/workflows/test.yml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Runs the pytest suite (IR generation + LLC compilation). Kernel verifier
2+
# tests additionally need passwordless sudo and a kernel with BTF/BPF
3+
# enabled, which isn't guaranteed on every runner, so we probe for working
4+
# sudo first and only attempt them if it's there.
5+
#
6+
# `push` is scoped to master only: branches here live in this repo rather
7+
# than forks, so a push to a branch with an open PR would otherwise fire
8+
# both `push` and `pull_request` for the same commit, running everything
9+
# twice. `pull_request` covers feature branches; `push` still gives master
10+
# a post-merge check.
11+
12+
name: Test
13+
14+
on:
15+
workflow_dispatch:
16+
push:
17+
branches: [master]
18+
pull_request:
19+
20+
jobs:
21+
test:
22+
name: Test
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v7
26+
27+
- uses: actions/setup-python@v7
28+
with:
29+
python-version: "3.12"
30+
31+
- name: Install system dependencies
32+
run: |
33+
sudo apt-get update
34+
sudo apt-get install -y lsb-release wget software-properties-common gnupg linux-tools-common linux-tools-generic
35+
36+
# Ubuntu's default `llvm`/`clang` packages (LLVM 18 on noble) are
37+
# too old to assemble the IR llvmlite>=0.49 emits: llvmlite's
38+
# ArgumentAttributes only knows the 'captures(none)' spelling of
39+
# the renamed 'nocapture' attribute, matching the LLVM 22.1.0 it
40+
# bundles internally - and an llc from an older LLVM (verified:
41+
# 19 still rejects it as a parse error) can't read that attribute
42+
# in the .ll text. Install a matching-generation LLVM from
43+
# apt.llvm.org instead of the distro default, and make its tools
44+
# the ones found on PATH.
45+
wget https://apt.llvm.org/llvm.sh
46+
chmod +x llvm.sh
47+
sudo ./llvm.sh 22 all
48+
sudo ln -sf /usr/bin/clang-22 /usr/local/bin/clang
49+
sudo ln -sf /usr/bin/llc-22 /usr/local/bin/llc
50+
clang --version
51+
llc --version
52+
53+
# bpftool isn't an installable package by itself on Ubuntu: it's a
54+
# virtual package provided by linux-tools-common + a kernel-flavor
55+
# linux-tools-<flavor> package. The runner's exact kernel version
56+
# has no matching linux-tools-<version> package, so the
57+
# update-alternatives symlink for `bpftool` doesn't get set up;
58+
# find whatever binary the generic-flavor package installed and
59+
# put it on PATH ourselves.
60+
bpftool_bin=$(sudo find /usr/lib/linux-tools* -name bpftool -type f 2>/dev/null | head -1)
61+
if [ -z "$bpftool_bin" ]; then
62+
echo "::error::Could not find a bpftool binary after installing linux-tools-generic"
63+
exit 1
64+
fi
65+
sudo ln -sf "$bpftool_bin" /usr/local/bin/bpftool
66+
bpftool version
67+
68+
- name: Install uv
69+
run: pip install uv
70+
71+
- name: Install project
72+
run: uv pip install --system -e ".[test]"
73+
74+
- name: Run test suite
75+
run: make test
76+
77+
- name: Check whether sudo is usable
78+
id: sudo-check
79+
run: |
80+
if sudo -n true 2>/dev/null; then
81+
echo "Passwordless sudo is available."
82+
echo "available=true" >> "$GITHUB_OUTPUT"
83+
else
84+
echo "No passwordless sudo on this runner; kernel verifier tests will be skipped."
85+
echo "available=false" >> "$GITHUB_OUTPUT"
86+
fi
87+
88+
- name: Run kernel verifier tests
89+
if: steps.sudo-check.outputs.available == 'true'
90+
run: |
91+
sudo -v
92+
make test-verifier

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ docs = [
4444
test = [
4545
"pytest>=8.0",
4646
"pytest-cov>=5.0",
47+
"ctypeslib2",
48+
# Pinned rather than left to ctypeslib2's own (unpinned) dependency: pip
49+
# installs the latest release by default, and its libclang API surface
50+
# can be newer than the system libclang (e.g. Ubuntu 24.04 ships
51+
# libclang-16), which fails with a LibclangError about an undefined
52+
# symbol. Older bindings against a newer libclang stay compatible, so
53+
# pin to an old-enough release instead of pinning apt's libclang.
54+
"clang==16.0.6",
4755
]
4856

4957
[tool.setuptools.packages.find]

tools/vmlinux-gen.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,57 @@ def step5_postprocess(self, input_file):
247247
# Replace ('_20', ctypes.c_char, 8) with ('_20', ctypes.c_uint8, 8)
248248
data = re.sub(r"(ctypes\.c_char)(\s*,\s*\d+\))", r"ctypes.c_uint8\2", data)
249249

250-
# below to replace those c_bool with bitfield greater than 8
251-
def repl(m):
252-
name, bits = m.groups()
253-
return (
254-
f"('{name}', ctypes.c_uint32, {bits})" if int(bits) > 8 else m.group(0)
255-
)
250+
# Some bitfields come out of clang2py with a declared width that
251+
# exceeds their own base type's bit width (e.g. a 15-bit field typed
252+
# as ctypes.c_ubyte, which only has 8 bits) - ctypes rejects these
253+
# outright with "ValueError: number of bits invalid for bit field".
254+
# Widen the base type to the smallest standard integer type that can
255+
# actually hold the declared width.
256+
bitfield_type_widths = {
257+
"c_bool": 8,
258+
"c_byte": 8,
259+
"c_ubyte": 8,
260+
"c_int8": 8,
261+
"c_uint8": 8,
262+
"c_short": 16,
263+
"c_ushort": 16,
264+
"c_int16": 16,
265+
"c_uint16": 16,
266+
"c_int": 32,
267+
"c_uint": 32,
268+
"c_int32": 32,
269+
"c_uint32": 32,
270+
"c_long": 64,
271+
"c_ulong": 64,
272+
"c_longlong": 64,
273+
"c_ulonglong": 64,
274+
"c_int64": 64,
275+
"c_uint64": 64,
276+
}
277+
promoted_type_for_width = {
278+
8: "c_uint8",
279+
16: "c_uint16",
280+
32: "c_uint32",
281+
64: "c_uint64",
282+
}
256283

257-
data = re.sub(r"\('([^']+)',\s*ctypes\.c_bool,\s*(\d+)\)", repl, data)
284+
def widen_oversized_bitfields(m):
285+
name, base_type, bits = m.group(1), m.group(2), int(m.group(3))
286+
type_width = bitfield_type_widths.get(base_type)
287+
if type_width is None or bits <= type_width:
288+
return m.group(0)
289+
for width in (8, 16, 32, 64):
290+
if bits <= width:
291+
return (
292+
f"('{name}', ctypes.{promoted_type_for_width[width]}, {bits})"
293+
)
294+
return m.group(0)
295+
296+
data = re.sub(
297+
r"\('([^']+)',\s*ctypes\.([a-zA-Z0-9_]+),\s*(\d+)\)",
298+
widen_oversized_bitfields,
299+
data,
300+
)
258301

259302
# Remove ctypes. prefix from invalid entries
260303
invalid_ctypes = ["bpf_iter_state", "_cache_type", "fs_context_purpose"]

0 commit comments

Comments
 (0)