-
-
Notifications
You must be signed in to change notification settings - Fork 415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
I32.clz_unsafe is *really* unsafe #2895
Comments
On a different CPU (Core i7-980X), again with a native build, it's more obvious what is going on. The disassembly looks like this:
This CPU does not update the destination register when
So the generated code immediately performs an invalid pointer read. |
@Praetonus, do you have any thoughts on this? |
What's the status here? I'm a bit surprised by the lack of reaction, considering that this is a memory safety issue. |
Unfortunately the last two sync calls had to be cancelled. We usually discuss, update and act on issues like this during sync. Also, during the last months most core contributors have been very busy with other stuff (like day-work) unfortunately, so not only this issue did not receive the treatment it deserves. I myself am not very familiar with those parts of llvm or the assembly that is generated from it. The reason for having those unsafe operations is indeed a performance advantage over the safe variants afaik. If your reproducer would have shown only some undefined behaviour with some surprising result value, that would have been fine by me, but a segfault is a whole lot of a different story to me. I am not against removing them, but this is a decision that should be made ideally by the full core team. |
Ideally, this shouldn't result in a segmentation fault. I say "ideally" because LLVM currently doesn't provide the framework that we'd need for ideal semantics. See this RFC for the semantics that I'd like to see in the language (note that the user-exposed side of the RFC (i.e. Now while the current situation isn't ideal, I don't think it is bad. Unsafe operations are designed to be used when you can prove by some other means that your operation is safe. For example, when you have some pre-processing that is guaranteed to produce values in a given range. In your case, you should use the safe
I don't think it would be a good solution. If your unsafe operation is in a cold path I agree that the gain will be extremely minor. But if it is in the middle of an extremely hot path (for example, a small loop that gets executed several thousands time per seconds) then you might see some improvements. The workflow when dealing with unsafe operation should be to use safe operations by default, and to only consider using an unsafe operation when your benchmarks show that it would result in a significant performance gain and when you can prove by some other means that the operation is safe. |
I was waiting for @Praetonus to chime in to see if he had ideas about fixing this. If this can't be fixed, I would advocate for removing the affected operations from the standard library (and also removing the syntax sugar operators), since memory safety is an important guarantee for Pony. As far as I'm concerned, the only time we're allowed to drop the memory safety guarantee is when the user invokes FFI. Any standard library code that invokes FFI internally should be wrapped in such a way that we can guarantee that no user invocation of those abstractions should trigger memory unsafety problems - the rest of the standard library is set up very carefully to keep this held true. I don't doubt that these unsafe operations have value for performance optimizations in the hot path, but you can still make those optimizations by invoking the LLVM intrinsics via FFI yourself. For a more convenient approach, my recommendation would be to make a third-party library package available that provides convenient wrappers for the memory-unsafe FFI invocations and gives obvious warnings about what you're expected to enforce in your code. If we can keep some of the value-unsafe operators that don't suffer memory safety / crash issues, I'm fine with keeping those around in the standard library - I just want to cast out anything that has this issue. |
An implementation as a third-party package won't be possible. The code generation of most unsafe operations requires compiler support since it uses LLVM instructions, not intrinsic functions. If we really want to remove them from the standard library, then we'd need to come up with a solution to allow specific non-builtin packages to use I think a good compromise would be to remove the operator sugar and to stress out in the documentation that if you're writing |
Hmm... I suggested it because I saw Could we maybe do simulated LLVM intrinsics for the other LLVM instructions, in such a way that they are parsed and treated like FFI calls, but we special-case those specific strings during codegen to use the instruction instead of an intrinsic? Something like: @"unsafe.add.i8"[I8](a, b) Interestingly, using faux-FFI in this way would also give us a convention to use to let folks who "know what they're doing" do no-overhead pointer math: @"unsafe.ptradd"[Pointer[U8]](ptr, offset) All while still adhering to the existing law that Pony is memory-safe until you start using the FFI syntax in your code. And we can use the existing package-whitelisting features to control which packages are allowed to do these unsafe things. I might file an RFC about this. |
It's worth noting that I toyed with these concepts a bit before (using real FFI instead of faux-FFI) - I created an The bummer was that this kind of unsafe escape hatch could never be zero-cost in terms of performance without being built into the compiler. Doing the faux-FFI |
For some other unsafe LLVM opcodes, it may be possible to get deterministic but unspecified results by emulating what the target does at the LLVM level. For example, if the target reduces 32-bit shifts modulo 32, you can generate code to do that, and LLVM will optimize the masking operation away if this is performed implicitly by the hardware. But for CLZ, that option doesn't exist, as far as I can tell. There are probably some unsafe operations where there is a performance impact from safe behavior, given the current state of LLVM (conversions from floating point to integers come to my mind). But for CLZ, I find that hard to believe because the precondition is easy to check, and in many cases, the compiler can determine the result anyway. On x86, performance of the instruction does not seem to be great anyway and a dynamic check (essentially a constant load plus CMOV) would mostly contribute to icache footprint. |
Consider this example program:
(This can undoubtedly be reduced further, but it will be really brittle.)
When I run it, I get this:
I believe the reason is this: LLVM treats undefined as really undefined. The bounds check
y < 32
is optimized away, and on some CPUs (mine is a Core m7-6Y75), when compiling natively,clz_unsafe
is implemented aslzcnt
, which yields 32 for a zero input value. The subsequent code gets confused by this because according to LLVM semantics, the value 32 is impossible here. However, this is not an LLVM bug—it does what the Pony compiler instructs it to do.My compiler:
The reproducer is probably brittle, so unless you have the same environment, it will not work.
The same issue applies to all unsafe arithmetic operations, with different reproducers. (
clz_unsafe
was literally the first one I tried.) I think it would be reasonable to simply remove all the unsafe arithmetic methods because the performance benefits will be extremely narrow.The text was updated successfully, but these errors were encountered: