Ask BTF which pointers the kernel is not allowed to follow - #55
Merged
Merged
Conversation
`struct file *f` and `char __user *buf` are both pointers, and the second one
is a pointer the kernel may not dereference. That difference is invisible in a
memory layout, because an annotation changes no offset and no size. It is a
rule about who may follow the pointer, and it is the rule behind a large share
of the bugs a lesson about system calls is about.
The reader printed the annotation in a type name and there was no way to ask
about one. Now there is. `kxray/btf/tags.py` holds what each annotation
promises and how you are supposed to reach the thing behind it, `Btf.tags`
answers which ones a type carries, and `Btf.annotated("rcu", "task_struct")`
answers which fields of a struct carry one.
Two things it refuses to answer, both on purpose. `__iomem` is a sparse
annotation the compiler drops, so BTF never sees it and the answer is a
refusal naming where to look instead. A blob with no type tags anywhere is
refused too, because a kernel built by a toolchain that does not emit them
looks exactly like a kernel whose structs are not annotated, and an empty list
reads as a no when the truth is a shrug.
Also adds `Btf.enum` and `Btf.enum_name`, since reading a number back into the
constant the source calls it is the direction a lesson needs.
The fixture gained `demo_annotated`: four pointers, three annotated and one
not, all the same size at the offsets you would expect. That is five more
types in `tiny.btf`, which is the one line that moved in the baseline.
tamnd
force-pushed
the
btf-annotations
branch
from
September 5, 2026 16:19
bfcd6c7 to
250e0a2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
struct file *fandchar __user *bufare both pointers. The second one is a pointer the kernel is not allowed to dereference, and nothing about that shows up in a memory layout, because an annotation changes no offset and no size. It is a rule about who may follow the pointer, and it is the rule behind a large share of the bugs a lesson about system calls is about.The reader already printed the annotation as part of a type name, so a layout table showed
char __user *. There was no way to ask about one. This adds the asking.That is the handwritten fixture and not a kernel, because there is no built kernel here yet. The same three calls are what a lesson will make against
/sys/kernel/btf/vmlinux, where the counts run to thousands andtask_structhas a screenful of__rcufields in it.kxray/btf/tags.pyholds the five annotations, what each promises, and how you are supposed to reach the thing behind the pointer.Btf.tags(type_id)answers which ones a type carries,Field.tagsputs them on every field of a layout, andBtf.annotated(tag, struct)answers the question a lesson asks.The two refusals
__iomemis not in BTF and never will be. It is a sparse annotation, defined asaddress_space(__iomem)under__CHECKER__and as nothing at all otherwise, so the compiler drops it before BTF is written.__user,__rcuand__percpugo throughBTF_TYPE_TAGand survive. So asking for__iomemraises, with the reason and with where to look instead, rather than returning an empty list. An empty list reads as "there are none" and the truth is "the file cannot say".The second refusal is the one I would have got wrong. A type tag reaches BTF only when the compiler that built the kernel emits it, and an image built by a toolchain that does not has no tags anywhere in it. Every question then answers empty, which looks exactly like a kernel whose structs happen not to be annotated. So
annotatedcheckstag_counts()first and refuses outright on a blob with no tags, saying it is almost always the toolchain. A reader who gets an empty list back from a kernel image should be told which of the two they are looking at.How it walks
The annotation is written on the pointee, not on the pointer.
char __user *bufis a pointer to a char that lives in user memory, so in BTF the chain isptrthentype_tagthenchar. Every question anybody asks is about the field, sotags()walks through pointers as well as through typedefs and const, and reports a tag found anywhere on that chain. A pointer to a pointer into user memory counts, which is right: it is still a field you may not follow without copying. There is a test for that shape and one for a tag behind a typedef.Also here
Btf.enum(name)andBtf.enum_name(name, value). The values were parsed and reachable only through the raw type record. Reading a number back into the constant the source calls it is the direction a lesson needs, because the trace, the/procfile or the crash dump hands you the number.The fixture
demo_annotatedis four pointers, three annotated with__user,__rcuand__percpuand one annotated with nothing. All four are eight bytes at the offsets you would expect, which is the point of it, and the fourth is there because a test where every field matches proves less than one with a control in it.That is five more types in
tiny.btf, so the baseline moved from 39 to 44 on that one artefact. Nothing else in it changed and no other number moved.Testing
19 new tests. The gate is green: ruff, 500 or so pytest tests, node, and all fourteen checkers.