Skip to content

Tracking Issue for "unsafe blocks in unsafe fn" (RFC #2585) #71668

Open

Description

@nikomatsakis

View all comments

This is a tracking issue for the RFC "unsafe blocks in unsafe fn" (rust-lang/rfcs#2585).
The lint unsafe_block_in_unsafe_fn is stable, but the RFC proposes some further things we might want to do.

About tracking issues

Tracking issues are used to record the overall progress of implementation.
They are also uses as hubs connecting to other relevant issues, e.g., bugs or open design questions.
A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature.
Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.

Steps

  • Introduce an opt-in lint that, when enabled, causes unsafe blocks in unsafe functions to be considered required, and warns if they are absent when an unsafe operation is performed.
  • Stabilization PR in Rust 1.52.0 (Stabilize unsafe_op_in_unsafe_fn lint #79208)
  • Include a suggestion with the lint that can insert required unsafe blocks. This could be as simple as adding a block across the entire function, though more granular insertion is probably better. (Add MVP suggestion for unsafe_op_in_unsafe_fn #112017)
  • Adjust documentation to describe the (somewhat unusual) effect of the lint, and to describe the possibility that the lint will be enabled default (see instructions on rustc-dev-guide)
  • Write a blog post describing the change, perhaps?

Unresolved Questions

  • What is the timeline for adding the lint, and cranking up its default level?
  • Should the default level depend on the edition?
  • Should we ever make this deny-by-default or even a hard error, in a future edition?
  • Should we require cargo fix to be able to do something about this warning before making it even warn-by-default? And how precise should it be?

Implementation history

Activity

  1. changed the title [-]Tracking Issue for XXX[/-] [+]Tracking Issue for "unsafe blocks in unsafe fn" (RFC #2585)[/+] on Apr 29, 2020
  2. nikomatsakis commented on Apr 29, 2020

    @nikomatsakis
    ContributorAuthor
  3. nikomatsakis commented on Apr 30, 2020

    @nikomatsakis
    ContributorAuthor

    A relevant note towards the implementation:

  4. LeSeulArtichaut commented on May 3, 2020

    @LeSeulArtichaut
    Contributor

    I'll try to work on the implementation!

  5. RalfJung commented on May 27, 2020

    @RalfJung
    Member

    If I understand https://boats.gitlab.io/blog/post/ringbahn/ correctly, that API would also benefit greatly from not treating the body of an unsafe fn as unsafe {}: there is a trait with an unsafe fn, and

    It’s important to understand what an unsafe methods means: unsafe methods are unsafe to call - meaning that callers must guarantee additional invariants when calling them. However, they are safe to implement (as long as you don’t do anything unsafe inside of them). So when a user implements the Event trait, they are given additional guarantees about how this method will be called that normally they could not assume.

    @withoutboats did I understand this correctly?

  6. withoutboats commented on May 27, 2020

    @withoutboats
    Contributor

    @RalfJung I don't think its a particularly new situation. Basically, the guarantee that you are getting from it being unsafe are the guarantees you need to be able to call these unsafe methods in the SubmissionQueueEvent that is passed as an argument.

    In other words, an Event implementation is going to use probably 1 unsafe operation (calling one of these methods), which it can prove is correct based on the invariant that trait requires the caller to maintain. This isn't really different from any other unsafe code.

    What I'm trying to say with the blog post is that the Event trait hasn't introduced a new invariant for you to maintain, its introduced a new invariant that I maintain so its easier for you to call that unsafe method you need to call to get any work done.

    This is just how unsafe trait methods work in general, so its nothing new. But I agree that unsafe trait methods (where the invariant contract is specified separately from the implementer) are a compelling example of where unsafe blocks in unsafe fns seem beneficial.

  7. RalfJung commented on May 27, 2020

    @RalfJung
    Member

    What I'm trying to say with the blog post is that the Event trait hasn't introduced a new invariant for you to maintain, its introduced a new invariant that I maintain so its easier for you to call that unsafe method you need to call to get any work done.

    Is there a realistic safe implementation of that method that ignores the extra invariant provided by the caller, or is the expectation that most/all implementations will have to do something unsafe justified by that invariant?

  8. withoutboats commented on May 27, 2020

    @withoutboats
    Contributor

    Yea I believe some kinds of events should be safe to prepare because they only send Copy data into the kernel and so they don't need to worry about any kind of memory safety invariant.

  9. added 2 commits that reference this issue on May 29, 2020
  10. 158 remaining items

  11. horace-walsh commented on Feb 12, 2026

    @horace-walsh

    This is achieving the exact opposite of the intended effect, it's making the usage of unsafe riskier by making the "correct" usage pattern more verbose.

    Beforehand using unsafe fn automatically propagated the unsafe tag up the call chain until a "willing" frame wrapped the call in unsafe { ... }. It was a clever trick to use convenience for propagation.

    With this change there is no propagation. From the compiler output's perspective there is literally no difference between

    unsafe fn something() {
      unsafe { whatever() }
    }

    and

    fn something() {
      unsafe { whatever() }
    }

    which means there is nothing to force the programmer to add the unsafe fn tags. Why would they, they have to add the op unsafe either way.

    If the goal is to avoid "accidental" unsafe calls in unsafe functions (...as if that was a real concern...) then personally I would approach it from a different angle. For example force the usage of unsafe both at the op and the fn, unless a new tag e.g. unsafe_trusted { .. } is used for the op, marking the wrap. Yes, it's verbose. Yes, it's inconvenient. But at least it's actually safe as opposed to #71668, which is both inconvenient and less safe.

  12. RalfJung commented on Feb 12, 2026

    @RalfJung
    Member

    The reasoning for why we think this lint is a step in the right direction is spelled out in the RFC. The typical systematic way of dealing with unsafe code is entirely undermined without this lint.

    which means there is nothing to force the programmer to add the unsafe fn tags

    That already wasn't the case before, one could have added the unsafe block instead.

    We've also already established project consensus for this. It's not really up for debate any more.

  13. horace-walsh commented on Feb 12, 2026

    @horace-walsh

    That already wasn't the case before, one could have added the unsafe block instead.

    Indeed. However, previously the most common usage pattern was

    unsafe fn something() {
      whatever()
    }
    

    which was safe. This change essentially removes this pattern, which means programmers have to pick between two choices, and most of then will opt for

    fn something() {
      unsafe { whatever() }
    }
    

    I would even argue that newcomers will not understand the difference between the two choices. It will just be a compiler error that they have to "make go away".

    Essentially this change will make programmers switch from a previously mostly safe tradeoff to an unsafe default.

    We've also already established project consensus for this. It's not really up for debate any more.

    I also understand that. However actual engineers will encounter these issues through the lint, not by browsing RFCs. Like I just did.

  14. Thomasdezeeuw commented on Feb 12, 2026

    @Thomasdezeeuw
    Contributor
    unsafe fn something() {
      whatever()
    }
    

    [...]

    fn something() {
      unsafe { whatever() }
    }
    

    I would note that two examples mean two different things. The first says that function something is unsafe and the caller needs to uphold some kind of constraint. The second says that something is safe and that the function itself is upholding the required constraints to call whatever.

    In my opinion what you want is the following (which I think the RFC is suggesting, but perhaps the lint doesn't make clear enough).

    unsafe fn something() {
      unsafe { whatever() }
    }
    
  15. RalfJung commented on Feb 12, 2026

    @RalfJung
    Member

    However actual engineers will encounter these issues through the lint, not by browsing RFCs. Like I just did.

    Actual engineers were confused by the old setup and misunderstood what unsafe even does and means. From the feedback we got, most people consider this change an improvement. In particular most people understand very well what it means to mark a function as safe vs unsafe. No system is perfect, obviously, and documentation can always be improved.

    https://doc.rust-lang.org/nightly/std/keyword.unsafe.html is a good starting point.

  16. horace-walsh commented on Feb 12, 2026

    @horace-walsh

    I would note that two examples mean two different things.

    Yes, but not from a compiler feedback perspective.

    Previously:
    unsafe fn { op }: mostly opted for default, unsafe propagates
    fn { unsafe op }: inline unsafe blocks are slightly less convenient, therefore force the programmer to think about whether it's actually safe to call op
    unsafe fn { unsafe op }: almost noone used this

    Now:
    fn { unsafe op }: what most people will opt for when writing new code.
    unsafe fn { unsafe op }: almost noone will use this for new code. Only for the lint this change introduced in existing code. Maybe not even that.

    In my opinion what you want is the following (which I think the RFC is suggesting, but perhaps the lint doesn't make clear enough).

    No, that's clear enough, as should be to anyone who bothers to look up the docs. My point is exactly that this pattern (which should be the "correct" one) is not enforced and unclear why it would be needed just based on the compiler feedback. It is also inconvenient, therefore people are less likely to use it.

  17. clarfonthey commented on Feb 12, 2026

    @clarfonthey
    Contributor

    Side note, what is this issue tracking, currently? This lint is already enabled by default on the 2024 edition and it's unclear what else is left to do.

  18. RalfJung commented on Feb 12, 2026

    @RalfJung
    Member

    Apparently

    • Adjust documentation to describe the (somewhat unusual) effect of the lint, and to describe the possibility that the lint will be enabled default
    • Write a blog post describing the change, perhaps?

    Not sure if any of those are actually still planned.

  19. clarfonthey commented on Feb 12, 2026

    @clarfonthey
    Contributor

    So:

    1. https://doc.rust-lang.org/edition-guide/rust-2024/unsafe-op-in-unsafe-fn.html
    2. https://blog.rust-lang.org/2025/02/20/Rust-1.85.0/

    ?

    (The post announcing the edition doesn't go into detail on this specifically, but it would be assumed that this is enough to qualify as "announcing" it.)

  20. 9thyear2 commented on May 7, 2026

    @9thyear2

    I would note that two examples mean two different things.

    Yes, but not from a compiler feedback perspective.

    Previously: unsafe fn { op }: mostly opted for default, unsafe propagates fn { unsafe op }: inline unsafe blocks are slightly less convenient, therefore force the programmer to think about whether it's actually safe to call op unsafe fn { unsafe op }: almost noone used this

    Now: fn { unsafe op }: what most people will opt for when writing new code. unsafe fn { unsafe op }: almost noone will use this for new code. Only for the lint this change introduced in existing code. Maybe not even that.

    In my opinion what you want is the following (which I think the RFC is suggesting, but perhaps the lint doesn't make clear enough).

    No, that's clear enough, as should be to anyone who bothers to look up the docs. My point is exactly that this pattern (which should be the "correct" one) is not enforced and unclear why it would be needed just based on the compiler feedback. It is also inconvenient, therefore people are less likely to use it.

    i tend to agree, i just came out of learning rust and i ended up asking myself when should i use an:

    unsafe fn something () {
        unsafe {
            do_something()
        }
    }
    

    over

    fn something () {
        unsafe {
            do_something()
        }
    }
    

    as there is no constraint to stop me using the second pattern over the first (i mean i defined one for myself (a personal rule (anytime i use raw pointers, it needs to be in an unsafe fn)), but to my knowledge that's not upheld by the compiler

    your right and people don't want to admit it, people will begin to prefer the second pattern over the first one (not just devs, but those who are just getting started with rust too (who will just want to make the warnings and error disappear))

    whats to stop people from defining safe functions that use a bunch of unsafe blocks (and making a "safe" function that's actually turns out to be unsafe)? while that was possible before. Requiring unsafe blocks in unsafe functions will make people prefer to just do that, as there would be almost no distinction when writing the code between an unsafe function and a regular function, as both would require unsafe blocks

    what is needed for this to make sense is some forced use of unsafe functions, but if that doesn't accompany this. than this alone doesn't make sense

    people are just like lightning... they take the path of least resistance. In this case that would be not typing the extra 7 characters unsafe right before fn

    from a human psychology standpoint, combined with rusts goals... this doesn't make any sense alone

  21. SimonSapin commented on May 8, 2026

    @SimonSapin
    Contributor

    as there is no constraint to stop me using the second pattern over the first […] but to my knowledge that's not upheld by the compiler

    unsafe is all about things the compiler cannot entirely check on its own. That’s why it’s called unsafe. I’m sorry but when using unsafe at all there is no substitute for learning the meaning of these two patterns and using them accordingly. Namely: an unsafe fn is unsafe to call: the caller is responsible for maintaining some invariant (which should be documented). fn is safe to call with any parameter it accepts, and if an unsafe {…} block is used the function body itself is responsible to uphold the relevant invariants (ideally also with a comment that explains how).

  22. 9thyear2 commented on May 8, 2026

    @9thyear2

    compiler cannot entirely check on its own

    i wasn't referencing the compiler checking for safety, but what is being called within the code (eg. not allowing the use of raw pointers outside a specific pattern as i described in my personal rule)

    the problem i was pointing out... is previously there were 2 ways of writing unsafe code:

    unsafe fn something () {
        do_something()
    }
    

    and

    fn something () {
        unsafe {
            do_something()
        }
    }
    

    this would cause a dev to stop and think what is more appropriate because of the differences between how the code could be wrote

    but now you did the meme ("they're the same picture"):

    fn something () {      // <--- unsafe before fn or not... doesn't matter
        unsafe {
            do_something()
        }
    }
    

    regardless whether its safe so not, that stop and pause is now removed, as there is no difference in the code between writing a safe or unsafe function

    you might define one in the docs, but the docs aren't right in your face as your writing the code. and most tutorials that people watch use an older version of rust where this wasn't a thing

    so as stated before... when presented with this the majority of people will act just like lightning, and take the path of least resistance (or use this all the time):

    fn something () {
        unsafe {
            do_something()
        }
    }
    

    and that personal rule i mentioned... was for me to introduce a differentiator back between the 2 styles (so i have a stop and pause)

    you should look at doing the same

  23. ojeda commented on May 8, 2026

    @ojeda
    Contributor

    Just to clarify: there are safe functions that use unsafe code inside them, and that is fine and expected -- your raw pointer personal rule sounds like it will make you mark functions as unsafe that should have been safe.

    One could argue that safe fn should have been introduced as a way to opt-out, but the current way is simpler and more regular, in the sense that each of the positions of the keyword does something orthogonal to the other.

    I would suggest using Zulip to discuss this though, since there are probably quite a lot of subscribers in this issue and any change around this would require a lot of discussion.

    Thanks!

  24. briansmith commented on May 8, 2026

    @briansmith
    Contributor
    fn something () {      // <--- unsafe before fn or not... doesn't matter
    

    It does matter. "unsafe fn" means that users of your function need to uphold safety invariants that the Rust language doesn't enforce, when calling your function. fn without unsafe means that there is nothing special that the caller needs to do. This is why every combination, all of the following, is needed:

    unsafe fn f() { unsafe { ... } }`
           fn f() { unsafe { ... } }
           fn f() { /*no unsafe*/ { ... } }
    unsafe fn f() { /*no unsafe*/ { ... } }
    

    I guess writing the documentation that clarifies this, exactly to help people who find it confusing, is the main reason why this issue is still open (bullet point "Adjust documentation to describe the (somewhat unusual) effect of the lint". Not really any reason to stay subscribed to this for anybody not interested in that.

  25. 9thyear2 commented on May 8, 2026

    @9thyear2

    It does matter. "unsafe fn" means that users of your function need to uphold safety invariants that the Rust language doesn't enforce, when calling your function.

    why do you guys insist on misinterpreting what i was saying on purpose

    wasn't referring what its purpose was, it was to simplify it so i didn't have to put 2 code examples with a 7 character difference

    it almost feels like what im saying is getting taken out of context to reaffirm this decision

    i was trying to be a little more professional and not use an actual meme, but since you insist on that here you go then:

    Image -

    all i was saying is a subtle difference in how you write the code will cause someone to pause and think about how best to proceed (yes my personal rule isn't perfect, but neither is expecting everyone to have the rust docs pulled up on a second screen to make sure the they are using each pattern correctly (or memorize the docs))

    what i was saying was introduce a subtle difference between the 2 styles again (my personal rule was an example (hence the word "personal")), but at least is causes me to take a pause and think what i am doing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    B-RFC-approvedBlocker: Approved by a merged RFC but not yet implemented.C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCS-tracking-impl-incompleteStatus: The implementation is incomplete.T-langRelevant to the language teamdisposition-mergeThis issue / PR is in PFCP or FCP with a disposition to merge it.finished-final-comment-periodThe final comment period is finished for this PR / Issue.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions