Re: __builtin_memcpy behavior


Toke Høiland-Jørgensen
 

"Tristan Mayfield" <mayfieldtristan@...> writes:

Thank you to both Andrii and Toke! It's been extremely helpful to read
your responses. Having conversations like these really helps me when I
go into the source code and try to understand the overall intent of
it. I'm going to try and summarize the conversation to confirm my
understanding.

bpf_probe_read() will read any valid kernel memory (nothing new here).
If the memory is already available to be read in the program (e.g. in
tracepoint args), then __builtin_memcpy can be used and will
potentially throw attach-time errors if reading structs incorrectly
(for some reason I don't think we clarified).
OK, I'll try to explain this one:

Think of __builtin_memcpy() as a macro: it just compiles down to regular
program instructions copying the memory (i.e., these two are roughly
equivalent, modulo any optimisations the compiler might make):

x = y;
__builtin_memcpy(&x, &y, sizeof(x));

The verifier will check the resulting memory access instructions, to
make sure you're not reading or writing out of bounds for whatever
variable you're reading from / writing to. E.g., if you're reading from
a context pointer, the verifier will know the size of the context object
and make sure you only dereference up to the memory address ctx +
sizeof(*ctx).

CO-RE can guarantee valid memory reads because of the nature of being
able to check offsets and relocations at load time rather than attach
time or just returning garbage data with no errors.
Yes, that's basically what it boils down to. It works like this: What
CO-RE does (for structs) is add some more information to the compiled
binary so that you can reference struct members by name instead of
memory offset. So, normally if you write:

x = y->z;

that will compile to a load from 'y + offsetof(typeof(y), z)', with the
offset being computed at compile time. When you add the
preserve_access_index attribute, clang will record a relocation that
says you wanted the member named 'z' (and its type) by way of the BTF
information. libbpf will read that at load time, and compute a new
'offsetof(typeof(y), z)' for the struct member as it exists in the
running kernel, so that if the layout has changed, you'll still get the
right offset. The load instruction in the byte code is then rewritten
with this new offset.

This means that by the time the bytecode is loaded into the kernel, it
has already been rewritten, so the kernel bounds check is still the same
- it'll just check that the memory you read is inside the size of the
structure; but because the offsets have been fixed up, the end result
you won't get out-of-bound errors - i.e., you might say that passing the
bounds check is an implicit effect of the CO-RE rewriting.

-Toke

Join iovisor-dev@lists.iovisor.org to automatically receive all group messages.