See https://lists.llvm.org/pipermail/llvm-dev/2021-December/154249.html for the RFC itself
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Unit Tests
Event Timeline
llvm/docs/LangRef.rst | ||
---|---|---|
20833 | *a memory region Maybe even something like: annotates a pointer with memory region that is accessible through that pointer. | |
20842 | Do we want to specify that end_offset > begin_offset ? | |
20868–20870 | I have a hard time parsing that sentence, did you mean something like: The only ``inbounds`` addresses that can be computed from pointer ``%ptr`` are those that lie within offsets ``[-8, 24]`` from that pointer. Other offsets will result in a :ref:`poison value <poisonvalues>`. | |
20869 | Given the semantics above, 24 should be excluded: [8,24) |
llvm/docs/LangRef.rst | ||
---|---|---|
20842 | end_offset can not be required to be bigger than begin_offset. Likewise, i don't think we can specify that What i think is something that still needs to be better clarified, I guess, given that end_offset specifies the offset from the pointer | |
20869 | Hm, no, why? Is the end pointer not inbounds? |
llvm/docs/LangRef.rst | ||
---|---|---|
20842 |
In that base, begin_offset is -32 and end_offset is 0, right ? So we still have begin_offset <= end_offset.
Yes you're right, obviously for the case of int a[0]. BTW there is an inconsistency between GNU C and C99 on this for flexible array members: in the former case we should not emit an annotation for this while we should for the latter.
My vote goes to begin_offset >= 0 => memory region begins after the pointer. |
llvm/docs/LangRef.rst | ||
---|---|---|
20852–20855 | I'm still confused as to why we need that restriction. For example, why do we want to disallow the following: %p2 = getelementptr inbounds i32, i32* %p1, i64 -42 %p2_bounded = @llvm.memory.region.decl.p0i8(%p2, 42, 43) %p3 = getelementptr inbounds i32, i32* %p2_bounded, i64 42 %v = load i32, i32* %p3, align 4 |
llvm/docs/LangRef.rst | ||
---|---|---|
10364–10371 |
|
llvm/docs/LangRef.rst | ||
---|---|---|
20852–20855 | At least as defined, %p2_bounded would already be poison in that case, because %p2_bounded would be before the declared memory region. The fact that %p3 would point back into the memory region doesn't matter in that case, because the pointer is already poison. | |
20896 | Doesn't this sentence contradict the first sentence in "Semantics"? If you want to make a distinction between inbounds/non-inbounds, then I think you have to do that in terms of restricting the visible allocated object, rather than saying that any pointer based on it cannot be outside the range. That would mean that something like %p = memory.region.decl(%p0, 8, 16) would not be poison, though dereferencing it would be and doing an inbounds gep would be, while doing a non-inbounds gep by 8 and then dereferencing would be legal. | |
llvm/include/llvm/IR/Intrinsics.td | ||
1197 | While technically correct, annotating it Returned means that the intrinsic is simply going to be optimized away. You want to drop returned here and instead add the intrinsic to isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(), which handles various intrinsics of this kind. |
Rebased, addressed review notes.
Ping, i feel like i'd want to start making progress here.
llvm/docs/LangRef.rst | ||
---|---|---|
20896 | Hmm, i do not remember why i've added that footprint here. |
I still believe this should be a generic assume.passthrough intrinsic with an operand bundle for the specific use case.
I briefly described that here: https://lists.llvm.org/pipermail/llvm-dev/2021-December/154281.html
The intrinsic would also be speculatable, readnone, all the good stuff.
The resulting value has all the annotated properties *or* is poison.
This matches attributes on call sites and as such we can retain attributes from call sites:
call @foo(i8* %p, i8* align(16) nonnull %p);
will become
%arg1 = llvm.assume.passthrough(%p) ["align"(16), "nonnull"] call @foo(i8* %p, i8* align(16) nonnull %arg1);
just before inlining. This way the information is retained properly.
Note that we should already retain it with llvm.assume if there is a noundef as well.
Does anybody else have any thoughts/opinions on this?
Is that a blocker?
I'm guessing you also argue that the noalias stuff should likewise also be designed this way?
This concept of sub-objects also shows up in the 'restrict' stuff. The difference is that with 'restrict' the aliasing constraints are dynamic: the promise is that e.g. two (dynamic) accesses in the original program don't alias. Here the constrains are static.
Ideally we would have a single solution for all this stuff, but instead right now we have multiple disjoint solutions. Each one will have to go through the pain of patching all optimizers so they understand the new intrinsics so they don't throttle back. It's not ideal..
I just wish we could sit physically in some place for a few days and work on a solution once and for all.
It's also very hard to discuss designs when so much code has been written already (especially for the 'restrict' stuff).
Regarding this patch in particular, I would mention that the storage of the returned sub-object is shared with the parent object, just to make it explicit. The concept looks ok.
Again, my concern is that to be able to use this new intrinsic from clang by default will take a lot of work. You'll likely face many perf regressions before patching a bunch of optimizations.
Thanks for taking a look!
Right.
I just wish we could sit physically in some place for a few days and work on a solution once and for all.
It's also very hard to discuss designs when so much code has been written already (especially for the 'restrict' stuff).
FWIW, currently i don't have any further code for this.
Regarding this patch in particular, I would mention that the storage of the returned sub-object is shared with the parent object, just to make it explicit. The concept looks ok.
Done.
Again, my concern is that to be able to use this new intrinsic from clang by default will take a lot of work. You'll likely face many perf regressions before patching a bunch of optimizations.
I acknowledge this reality. I'm just not sure we have a better approach than that,
it would be good to come up with a "a solution once and for all",
but i'm not sure how that would look.
In general I like this and would be very happy if we had a better way of dealing with sub-objects in LLVM.
Looking at this from the CHERI perspective, it seems like we could enforce this intrinsic at runtime by lowering it the same way as we current do for the out-of-tree`@llvm.cheri.bounds.set()`. This intrinsic also creates a memory subregion, the only difference as far as I can see it is that negative start offsets have to be encoded by doing a negative GEP first since we only have a pointer+length argument. And of course that it is enforced at runtime by narrowing the bounds that are part of the CHERI pointer.
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | Do you envision this being used for all sub-object pointer creations? If so it might need a flag to disable it since it might break some C patterns such as container_of. According to https://godbolt.org/z/evTbejaMf the container_of macro results in an inbounds GEP, so with sufficient inlining things might break? About three years ago I spent quite a lot of time enforcing sub-object bounds at runtime using CHERI. Almost all code works just fine but there are things such as container_of() that require opt-out annotations. I wrote about the incompatibilities that I found in Chapter 5 of https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-949.pdf. TL;DR: not many changes needed - about 50 annotations across the entire FreeBSD source tree. Almost all annotations due to container_of or emulation of C++ inheritance in C. |
Thank you for taking a look!
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | I'm not sure what you mean by "all sub-object pointer creations". Roughly, front-ends should emit this intrinsic on some pointer with some bounds The one case we know of is C arrays within structs. |
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | By sub-object pointer creations I mean something like &obj->field. You could conceivable treat that as declaring a new sub-object bounded to just field. E.g. something like this: https://godbolt.org/z/bM7j1bxqs |
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | I think the question is slightly wrong. It's up to front-ends to decide when they can and can't emit this. If you are asking about https://godbolt.org/z/adq5EWx17, IOW, i do not believe that as per the current C/C++ standards wording each data member of a struct |
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | Yes absolutely agreed that this is purely up to the frontend, I just assumed you had plans to update clang to emit the new intrinsic. It has been a long time since I looked at the C standard with regards to subobjects but if I recall correctly you are right that this is not defined as being illegal. However, doesn't that also mean that you can access the member before/after an embedded array? |
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | Right. Having a pointer to the array member of a struct isn't going to do anything. |
llvm/docs/LangRef.rst | ||
---|---|---|
20876 |
The rules on pointer arithmetic in C++: http://eel.is/c++draft/expr.add#4, so https://godbolt.org/z/adq5EWx17 looks like UB to me in C++. C2x 6.5.6p9 reads similarly, so it also looks like UB to me in C. What language rules make you think otherwise? |
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | Hm, thank you for correcting me, that does not match my recollection from the previous time we discussed this. |
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | I believe that's also UB for the same reason -- the subtraction violates http://eel.is/c++draft/expr.add#4.3, so we can't say much about the resulting pointer value. Note, a slightly different case of using + 1 instead of - 1 is valid because any object can be treated as an array of one (https://eel.is/c++draft/basic.compound#3.sentence-12) and a one-past-the-end pointer is valid (http://eel.is/c++draft/expr.add#4.2) so long as it's not dereferenced. |
@aaron.ballman thank you!
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | Right, +1 (end pointer) is non-dereferenceable, and is fine. Hmmmm, that sounds too good to be true. Then i stand corrected, |
No. Though, we should put a TODO somewhere if someone comes looking for a nice project to cleanup our messes.
Awesome! There has been a lot of approval of the proposal, but does anyone want to rubber-stamp it before i land this?
I am happy with this but I feel like someone else should also approve it :)
llvm/docs/LangRef.rst | ||
---|---|---|
20876 | Not a sanitizer for x86, but you can use CHERI LLVM and compile with -cheri-bounds=subobject-safe to detect such violations at runtime. Code can then be run on QEMU or also on Arm's Morello boards that were recently released (if you happen to be one of the recipients). I believe that a sanitizer for non-CHERI hardware would require complete provenance (bounds) shadow memory for every pointer so would be a big engineering effort. |
FWIW, this sounds like a generally useful feature to me and I believe it can be used to better express some C and C++ semantics.
Any context on that? I feel like I missed something here...
Anyway, I find the overall wording here still a bit confusing. I would put more emphasis on the fact that this effectively restricts the "allocated object" to a certain offset range, which should have the following three effects:
- For non-inbounds GEPs, this should have no effect.
- For inbounds GEPs, if the GEP goes outside the range [ptr+begin_offset, ptr+end_offset], the GEP result is poison.
- For accesses, if the access is outside the range [ptr+begin_offset, ptr+end_offset-1], the behavior is undefined.
The current wording mostly emphasizes the middle point, but not so much the first and the last. And the fact that the "one past the end of the region" address is only valid for GEP inbounds but not for accesses is probably important for optimization purposes.
llvm/docs/LangRef.rst | ||
---|---|---|
20841 | nit: "offset to" -> "offset from"? | |
llvm/include/llvm/IR/Intrinsics.td | ||
1196 | nit: Is the ReadNone here actually meaningful if the whole intrinsic is already IntrNoMem? |
The TLDR is that i don't feel like contributing to projects that continuously shit on their fellow contributors.
Migration from phab to github pull requests will likely be my tipping point.
Anyway, I find the overall wording here still a bit confusing. I would put more emphasis on the fact that this effectively restricts the "allocated object" to a certain offset range, which should have the following three effects:
- For non-inbounds GEPs, this should have no effect.
- For inbounds GEPs, if the GEP goes outside the range [ptr+begin_offset, ptr+end_offset], the GEP result is poison.
- For accesses, if the access is outside the range [ptr+begin_offset, ptr+end_offset-1], the behavior is undefined.
The current wording mostly emphasizes the middle point, but not so much the first and the last. And the fact that the "one past the end of the region" address is only valid for GEP inbounds but not for accesses is probably important for optimization purposes.
It will be great to have this patch landed. It will help to rewrite D126533 and make progress. Alternatively, would it be OK for someone else to commandeer this patch?
Apologies for the community issues around phabricator and github pr. But I guess it will take time to solve.