-
Notifications
You must be signed in to change notification settings - Fork 13.4k
fn_cast!
macro
#140803
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Comments
So in such a world, the docs for the macro would say that this generates a new function? Because otherwise it seems like this list here has to account for the macro as well. The macro needs to be unsafe of course, since function arguments are still being transmuted. We could have the macro ensure that the signatures are ABI-compatible -- but this can only be fully checked during monomorphization. |
Well, yes it semantically creates a new function even if it has the same address. How exactly we word that is up to debate. I guess we might not want provenance for function pointers (?), so if |
I mean, we could.^^ But yeah it's probably better if we avoid using provenance wherever possible. |
We have unsafe function pointers. So I wonder, should the macro call be unsafe, or should we be returning an
If we were to do these checks, I wonder whether we might want to support this as a coercion or let _: *const () = &(); Does it make any sense to allow?: let _: unsafe fn(*const ()) = |&()| (); |
I don't think I'd like to make |
The macro itself needs to be unsafe. Otherwise, how do people get a non-unsafe fn pointer? By transmuting the output of |
While the function signature change itself can’t cause UB without the function being called, asking the call sites to justify the safety of the implied arg/result transmutes leads to somewhat silly consequences:
It’s tempting to say: At the same time, there are cases where a safe function is type-punned into something that creates significant extra safety conditions for callers (e.g., type erasing So I think it’s probably most useful to consider “safe fn <-> unsafe fn” to be part of the type punning that |
Interesting. That's not how I think about it. I think of the point of the built-in as being to do something a lot smarter than what's otherwise possible so as to support CFI, in terms of modifying the list of signatures when it can, generating and using a trampoline only when needed, etc. If it were just about getting rid of a transmute, I don't think we'd do this.
Perhaps you could describe the use case you have in mind for when the cast function pointer will be safe to call. What's coming to my mind, in terms of practical use cases, are all ones where it would not be. |
Let me adjust my phrasing: yes, CFI compatibility is ultimately "the point" but I don't think this can be usefully separated from removing function pointer transmutes. To make CFI work, you need an intentional marker for "this specific function can also be called with this specific signature different from what its definition said" (which then enables e.g. generating the right trampoline if one is needed) rather than transmutes that leave you guessing whether the signature mismatch may be unintentional. Carving out a subset of such transmutes that are "still okay" after the introduction of
This example is a bit speculative for several reasons, but it's inspired by real code I'm working on. Consider a library that defines a trait for "fieldless impl<I: /* ... */, T> Array<I, T> {
fn for_each_with_index_erased<F: FnMut(I, &T)>(f: F) {
// SAFETY: `I` is a `repr(u8)` enum, so it's sound to transmute into u8
for_each_with_index_raw(&self.0, unsafe { fn_cast!(f) })
}
}
fn for_each_with_index_erased<T>(elems: &[T], f: fn(u8, &T)) {
for (i, elem) in elems.iter().enumerate() {
f(i as u8, elem);
}
} One reason this is speculative is I don't think there's a bound I could put on the type parameter |
Another reason why the above example is speculative: the proposal says that the function being cast can’t have any captures if it’s a closure. It’s not clear to me why that restriction would be needed. Couldn’t the compiler generate another closure type that has the same captures, is ABI-compatible with the original closure type, and implement the appropriate Of course this doesn’t have to be part of the initial feature but I’d like to know if it’s possible in principle or if there’s a fundamental problem I’m missing. |
In discussion with @Darksonn I toyed the idea that |
Interesting. As context for when we take up this nomination, @RalfJung, it'd probably be helpful if you could perhaps elaborate on that a bit. |
Not sure what to elaborate on? The question is which transmutes are allowed on the fn ptr you get out of the We could say that |
Would this carve-out for |
@hanna-kruppe It would be symmetric and transitive, yes. CFI would simply "skip" And of course it reduces the effectiveness of CFI in case these mismatches are unintended. @traviscross to elaborate a bit more, basically the goal is to keep code like this fully supported: fn foo(x: i32) -> i32 { x }
fn main() {
let ptr: fn(i32) -> i32 = foo;
let ptr: fn(NonZeroI32) -> NonZeroI32 = unsafe { transmute(ptr) };
ptr(NonZeroI32::new(15).unwrap());
}
The alternative is to say that even for this case, the program is considered to have erroneous behavior and one must use |
It's an interesting tradeoff. The more things that we want to work in that way, the more we weaken the CFI protections. Given our story about safety and security and whatnot, it would be kind of embarrassing if some exploit chain ended up leaning on Probably my sense is that people turning on CFI are going to prefer the most precise checking possible and probably will be willing to accept the churn and other work necessary for that. On the other hand, as a language matter, the alternate model of making everything CFI equivalent that we consider ABI equivalent does have some appeal. |
The other question is how much ABI compatibility for transparent wrappers is actually used in the ecosystem. As far as I know the biggest user by far of the current ABI compatibility rules is |
No, I don't know any examples. I assume we'll start seeing some once Miri enforces this. |
Not a blocker for adding |
Thought I had from the lang meeting: why isn't it ok to just normalize at least all the things in https://doc.rust-lang.org/std/primitive.fn.html#abi-compatibility from a CFI perspective? |
See rust-lang/unsafe-code-guidelines#489 for additional details and #128728 for a concrete issue. The short summary is that security people say that kind of normalization would leave too many doors open for an attacker to elevate UB elsewhere in the code into a full exploit. |
@scottmcm Normalizing all those things will normalize so many things that it leaves CFI as almost a no-op in practice. |
I believe we already do some such "normalization" in cases where Rust already considers it important to simply encode a I believe one of the consequences of that was the I could dig up the list of effective rules and originally intended to, but I can't provide the answer to what would be desired in the future in terms of mutations on that list. |
I do wonder how much this is inherently tied to C++'s whole idea of typed memory and such. Is it possible that in a language without strict aliasing and such, CFI is just inherently the wrong solution? |
Yes, but I want to make one point here: It's important to distinguish between the rules for CFI, and what transmutes we consider ABI compatible in the language. The point of #128728 is to make it so that if CFI rejects something, then the language must consider that case erroneous behavior (or UB). But it's okay for the language to disallow something that CFI accepts. CFI accepts things such as
I disagree. It isn't really about strict aliasing. The kernel quite explicitly passes |
As for #[cfi_encoding = "l"]
#[repr(transparent)]
pub struct c_long(pub core::ffi::c_long); is no longer treated like the inner type by CFI despite the |
Right, I was about to say the same thing. :) What we are defining here is the ceiling of what any future CFI mechanism can do while being compatible with Rust's semantics. It's entirely fine to declare things to be EB due to ABI mismatch that no current CFI tool (except for Miri) can catch because the distinction is lost before reaching codegen. @scottmcm I don't think this is very tied to a typed idea of memory, though it may come more natural in a language with typed memory. It's about mismatches between caller and callee during argument passing, which is not an in-memory operation, it's its own magic thing. If I were to define argument passing in MiniRust without any regard for reality, I would say it works like a transmute, and thus allow arbitrary signature mismatches as long as the size matches. This is a terrible model since many real-world ABIs do not actually correctly implement these semantics. In that sense, our ABIs already integrate a notion of "type" more deeply than Rust's memory model does -- it's more like we are passing values to functions, not in-memory representations. Now we basically have two options:
Maybe if there was a practical "memory type integrity" sanitizer enforcing C++'s ideas of typed memory, people would ask for Rust to be compatible with it somehow. But that's not a thing, and control flow integrity is much more narrow in scope -- I think most Rust code will be compatible with the EB rules described here without any adjustment. Function pointer transmutes are quite rare. |
Speaking of naive idealized semantics, we could use this to completely hide what ABI can and cannot do from the spec. We could say that by using From a spec perspective I would quite like this actually. :) |
I love that. Then the CFI case becomes just another ABI with its own weird rules. Is treating After all, CFI is already considered a target modifier, i.e. an ABI-modifying flag. |
Agreed. That is an appealing framing. |
Uh oh!
There was an error while loading. Please reload this page.
Since Rust 1.76 we document that it's valid to transmute function pointers from one signature to another as long as their signatures are ABI-compatible. However, we have since learned that these rules may be too broad and allow some transmutes that it is undesirable to permit. Specifically, transmutes that change the pointee type or constness of a pointer argument are considered ABI-compatible, but they are rejected by the CFI sanitizer as incompatible. See rust-lang/unsafe-code-guidelines#489 for additional details and #128728 for a concrete issue.
This issue tracks a proposed solution to the above: Introduce a new macro called
fn_cast!
that allows you to change the signature of a function pointer. Under most circumstances, this is equivalent to simply transmuting the function pointer, but in some cases it will generate a new "trampoline" function that transmutes all arguments and calls the original function. This allows you to perform such function casts safely without paying the cost of a trampoline when it's not needed.The argument to
fn_cast!()
must be an expression that evaluates to a function item or a non-capturing closure. This ensures that the compiler knows which function is being called at monomorphization time.As a sketch, you can implement a simple version of the macro like this:
This implementation should get the point across, but it is incomplete for a few reasons:
fn_cast!
should be improved to work with functions of any arity.fn(&T)
tofn(*const T)
is allowed because&T
and*const T
is treated the same by KCFI. The compiler could detect such cases and emit a transmute instead of a trampoline.By adding this macro, it becomes feasible to make the following breaking change to the spec:
Here, the change is that ABI-compatible calls are considered EB. However, even without the spec change the macro is useful because it would allow for a more efficient implementation of #139632 than what is possible today.
This proposal was originally made as a comment. I'm filing a new issue because T-lang requested that I do so during the RfL meeting 2025-05-07.
The text was updated successfully, but these errors were encountered: