Skip to content

Allow UnsafeCell in shared statics#152540

Open
madsmtm wants to merge 2 commits intorust-lang:mainfrom
madsmtm:unsafecell-shared-static
Open

Allow UnsafeCell in shared statics#152540
madsmtm wants to merge 2 commits intorust-lang:mainfrom
madsmtm:unsafecell-shared-static

Conversation

@madsmtm
Copy link
Contributor

@madsmtm madsmtm commented Feb 12, 2026

Background

Using a type in a (shared) static introduces a Sync bound on that type, to ensure that it is safe to share across threads.

This is sufficient, but overly restrictive: there exist other types which are safe to use directly in a static, including UnsafeCell and raw pointers (these types are !Sync primarily as a "hint" to avoid items containing them being Sync via auto traits, but they are by themselves not unsafe to share across multiple threads).

For example static NULL_INT: *const i32 = std::ptr::null(); would be perfectly valid. Another (very contrived) example can be found in this playground.

Proposal

I propose special-casing UnsafeCell<T: Sync> to allow it as the type in shared statics.

This would enable the following to compile:

static FOO: UnsafeCell<i32> = UnsafeCell::new(42);

fn main() {
    // SAFETY: No other threads are accessing FOO.
    unsafe { FOO.get().write(43) };
}

As an alternative to:

static mut FOO: i32 = 42;

fn main() {
    // SAFETY: No other threads are accessing FOO.
    unsafe { addr_of_mut!(FOO).write(43) };
}

While the following would still be an error (because MyCell is !Sync):

struct MyCell(UnsafeCell<i32>);
static CELL: MyCell = MyCell(UnsafeCell::new(42));

Unlike adding an unsafe impl<T: Sync> Sync for UnsafeCell<T>, this would not be a breaking change as far as I can tell, as library authors shouldn't rely on an arbitrary UnsafeCell not being shared elsewhere (APIs taking &UnsafeCell<T> can't be safe).

This is intended as a partial alternative to SyncUnsafeCell (tracking issue), which was introduced primarily to avoid having to use static mut, and secondarily to avoid the need for a manual Sync impl. (This PR would make the first item redundant but wouldn't change the second).

I have opened rust-lang/reference#2169 to update the reference, to allow you to see how things would look if this was stabilized.

Implementation

This PR introduces a new unstable trait AllowSharedStatic which is now used for the bound on shared statics instead of Sync. It is defined as:

#[lang = "allow_shared_static"]
pub unsafe trait AllowSharedStatic {}
unsafe impl<T: Sync> AllowSharedStatic for T {}
unsafe impl<T: Sync> AllowSharedStatic for UnsafeCell<T> {}

This trait is intended to be perma-unstable and not be exposed to users (at least not unless we find a compelling reason to do so). The trait could be implemented for other types in the future (such as *const T/*mut T or UnsafeCell<T: !Sync>).

TODO

  • Make this opt-in with an unstable feature, and create a tracking issue.
  • Measure performance impact, special-case in compiler if necessary.
  • Better diagnostics, we (probably?) want to keep using Sync's "on_unimplemented" messages, and AllowSharedStatic shouldn't appear in error messages.
  • Proper documentation.
  • Bikeshed naming.

I will do these if t-lang decides to go forwards with this idea.
r? compiler

This is a new trait that is used by the language in bounds for `static`
items instead of `Sync`. It is implemented by all `T: Sync`, as well as
by `UnsafeCell<T: Sync>`.
The only place where it was required by the language was for checking
usage in static variables (and that is now done by `AllowSharedStatic`),
all other usage in the compiler was for diagnostics.
@madsmtm madsmtm added T-lang Relevant to the language team T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Feb 12, 2026
@rustbot
Copy link
Collaborator

rustbot commented Feb 12, 2026

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

This PR modifies tests/auxiliary/minicore.rs.

cc @jieyouxu

Some changes occurred in compiler/rustc_codegen_gcc

cc @antoyo, @GuillaumeGomez

Some changes occurred in compiler/rustc_codegen_cranelift

cc @bjorn3

rust-analyzer is developed in its own repository. If possible, consider making this change to rust-lang/rust-analyzer instead.

cc @rust-lang/rust-analyzer

@rustbot rustbot added A-run-make Area: port run-make Makefiles to rmake.rs A-test-infra-minicore Area: `minicore` test auxiliary and `//@ add-core-stubs` S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Feb 12, 2026
@rustbot rustbot added T-clippy Relevant to the Clippy team. T-rust-analyzer Relevant to the rust-analyzer team, which will review and decide on the PR/issue. labels Feb 12, 2026
@madsmtm
Copy link
Contributor Author

madsmtm commented Feb 12, 2026

@rustbot label +I-lang-nominated

Nominating for t-lang:

Do you want to preserve the "shared static" == "requires Sync" relationship, or would you be open to extending that to UnsafeCell<T: Sync> as well (which is !Sync)?

This would allow writing static FOO: UnsafeCell<T: Sync>, and resolve a lot of the use-cases for SyncUnsafeCell.

I guess part of the motivation for this (and SyncUnsafeCell) is to avoid static mut, so an alternative here would also be to recommend static mut more strongly as "this is how you're supposed to use the language".

@rustbot rustbot added the I-lang-nominated Nominated for discussion during a lang team meeting. label Feb 12, 2026
@madsmtm madsmtm added S-waiting-on-t-lang Status: Awaiting decision from T-lang and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Feb 12, 2026
@rust-log-analyzer
Copy link
Collaborator

The job aarch64-gnu-llvm-20-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
REPOSITORY                                   TAG       IMAGE ID       CREATED       SIZE
ghcr.io/dependabot/dependabot-updater-core   latest    afc745c7535d   2 weeks ago   783MB
=> Removing docker images...
Deleted Images:
untagged: ghcr.io/dependabot/dependabot-updater-core:latest
untagged: ghcr.io/dependabot/dependabot-updater-core@sha256:faae3d3a1dedd24cde388bb506bbacc0f7ed1eae99ebac129af66acd8540c84a
deleted: sha256:afc745c7535da1bb12f92c273b9a7e9c52c3f12c5873714b2542da259c6d9769
deleted: sha256:64e147d5e54d9be8b8aa322e511cda02296eda4b8b8d063c6a314833aca50e29
deleted: sha256:5cba409bb463f4e7fa1a19f695450170422582c1bc7c0e934d893b4e5f558bc6
deleted: sha256:cddc6ebd344b0111eaab170ead1dfda24acdfe865ed8a12599a34d338fa8e28b
deleted: sha256:2412c3f334d79134573cd45e657fb6cc0abd75bef3881458b0d498d936545c8d
---
fmt: checked 6701 files
tidy check
tidy [rustdoc_json (src)]: `rustdoc-json-types` modified, checking format version
tidy: Skipping binary file check, read-only filesystem
tidy [style (compiler)]: /checkout/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs:3403: TODO is used for tasks that should be done before merging a PR; If you want to leave a message in the codebase use FIXME
tidy [style (library)]: /checkout/library/core/src/marker.rs:680: TODO is used for tasks that should be done before merging a PR; If you want to leave a message in the codebase use FIXME
tidy [style (library)]: /checkout/library/core/src/marker.rs:703: TODO is used for tasks that should be done before merging a PR; If you want to leave a message in the codebase use FIXME
tidy [style (library)]: FAIL
tidy [style (compiler)]: FAIL
tidy: The following checks failed: style (compiler), style (library)
Bootstrap failed while executing `--stage 2 test --skip tests --skip coverage-map --skip coverage-run --skip library --skip tidyselftest`
Command `/checkout/obj/build/aarch64-unknown-linux-gnu/stage1-tools-bin/rust-tidy --root-path=/checkout --cargo-path=/checkout/obj/build/aarch64-unknown-linux-gnu/stage0/bin/cargo --output-dir=/checkout/obj/build --concurrency=4 --npm-path=yarn` failed with exit code 1
Created at: src/bootstrap/src/core/build_steps/tool.rs:1612:23
Executed at: src/bootstrap/src/core/build_steps/test.rs:1365:29

Command has failed. Rerun with -v to see more details.
Build completed unsuccessfully in 0:00:50
  local time: Thu Feb 12 15:32:49 UTC 2026
  network time: Thu, 12 Feb 2026 15:32:50 GMT
##[error]Process completed with exit code 1.
##[group]Run echo "disk usage:"

@madsmtm madsmtm removed A-run-make Area: port run-make Makefiles to rmake.rs T-rust-analyzer Relevant to the rust-analyzer team, which will review and decide on the PR/issue. A-test-infra-minicore Area: `minicore` test auxiliary and `//@ add-core-stubs` T-clippy Relevant to the Clippy team. labels Feb 12, 2026
//
// See also TODO link.
#[unstable(feature = "allow_shared_static_trait", issue = "none")]
unsafe impl<T: MetaSized + Sync> AllowSharedStatic for UnsafeCell<T> {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this have a Sync bound on T? The argument for why this is sound (all accesses are unsafe) also work for arbitrary T.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intended for this PR to be the minimal first step. We can extend it to T: !Sync later (or during the unstable period).

@ChayimFriedman2
Copy link
Contributor

What is the advantage of this over the much simpler SyncUnsafeCell?

@madsmtm
Copy link
Contributor Author

madsmtm commented Feb 12, 2026

What is the advantage of this over the much simpler SyncUnsafeCell?

Simple is in the eye of the beholder.

To me, SyncUnsafeCell feels like a hack that exists because we want UnsafeCell to disable the automatic Sync impl on structs containing it (or we at least have to keep it that way for backwards compat). In a different world where Sync was not an auto trait, SyncUnsafeCell would not exist.

You could argue this PR is a hack too, but then it's just a choice between the least ugly hack ;)

(It's also not clear to me from just the name whether SyncUnsafeCell<*const i32> would be Sync or not).


As a more general problem, bifurcation of types like this is not without a cost, users are gonna have to learn this new type and crates in the ecosystem like zerocopy that wants to interoperate well with the standard library has to be updated to handle it.

@ChayimFriedman2
Copy link
Contributor

I claim that this PR is way more ugly hack and in fact, SyncUnsafeCell is not a hack at all. You're changing the language here, while SyncUnsafeCell is just a standard library type anyone can write and understand. The bar for language changes is much higher.

@RalfJung
Copy link
Member

RalfJung commented Feb 12, 2026

I think the ideal solution would be a language change that lets me unsafely say "yes I want this static with a !Sync type, just let me do it and make accesses unsafe". But that's probably a bit too heavy of a hammer. I like the idea of using the existing UnsafeCell instead of proliferating yet another wrapper type.

@RalfJung
Copy link
Member

That said -- I also like just using static mut for this. I am not yet entirely convinced there even is a problem left to solve here.

@madsmtm
Copy link
Contributor Author

madsmtm commented Feb 12, 2026

That said -- I also like just using static mut for this. I am not yet entirely convinced there even is a problem left to solve here.

Well, me neither, but I guess that's part of what I want to help figure out.

@traviscross traviscross added the P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang label Feb 13, 2026
@ais523
Copy link

ais523 commented Feb 16, 2026

One reason to prefer SyncUnsafeCell over a language change: although I frequently want to store unsafe cells in statics, in practice I'm normally putting them into some sort of wrapper that contains both the UnsafeCell itself and some sort of flag or semaphore that I'm using to prove the access sound. (Think Mutex but in contexts where the standard library mutex wouldn't work for whatever reason, e.g. lack of a normal operating system.)

Having a special case for UnsafeCell wouldn't help in this situation because the special case wouldn't carry throuhg the wrapper (although I guess you could add a #[derive(AllowSharedStatic)] if you really had to) – but putting a SyncUnsafeCell inside the wrapper works fine.

@RalfJung
Copy link
Member

That sounds like you should just unsafe impl Sync for Wrapper?

@ais523
Copy link

ais523 commented Feb 16, 2026

That's what I do currently as a workaround, but there's no automatic check that the trait bounds on the Sync implementation are correct (whereas using SyncUnsafeCell would automatically give me the correct bounds), so it's relying on manual inspection to make sure that they're written correctly.

(The same workaround works when you want the cell directly – you can implement SyncUnsafeCell yourself as a simple wrapper and unsafe impl Sync on it.)

@RalfJung
Copy link
Member

but there's no automatic check that the trait bounds on the Sync implementation are correct (whereas using SyncUnsafeCell would automatically give me the correct bounds),

Wdym? SyncUnsafeCell doesn't magically check anything. The compiler cannot tell you whether your unsafe code implements correct synchronization.

@ais523
Copy link

ais523 commented Feb 16, 2026

It's generally easy for me to verify that the SyncUnsafeCell is only accessed from one location at a time (thus implying that it's only accessed by one thread at a time), but it's much harder to verify that I haven't missed some other reason why the wrapper type might not be Sync (e.g. references elsewhere that are outside the cell).

If I write SyncUnsafeCell, ideally all that I would need to check is that I'm not making two concurrent accesses to the inside of the cell. Unfortunately, the current definition isn't quite sufficient for that: SyncUnsafeCell<T>'s Sync implementation logically should require T: Send (because you can send a T by swapping it into the unsafe cell on one thread and then swapping it out of the unsafe cell on another thread, even if there are no concurrent accesses to the cell), but the current implementation requires T: Sync instead (which is required only if the cell is used for concurrent read accesses, which not all cells are) and does not require T: Send (which is required if the cell is used for non-concurrent write accesses, which basically all cells can be). Perhaps SyncUnsafeCell needs to be split into two versions, one where the safety conditions on get_mut allow concurrent reads (and is Sync if T: Sync + Send) and one where they don't (and is Sync if T: Send)?

Meanwhile, if I write unsafe impl Sync, I need to check every possible reason why the type might not be Sync, including some that I might not have thought of – the impl overwrites all the compiler's Sync tracking and just sets the type immediately to Sync, whereas what I intended was to turn off one particular reason why the type might not be Sync. Or to put it another way, it's turning off all the language's concurrency safeguards, rather than just the specific safeguard that I know isn't necessary.

In any case, the fact that there seem to be two reasonable different ways to define SyncUnsafeCell (neither of which matches the current definition) makes me even more suspicious of adding this as a language feature, as we would have to decide which definition of SyncUnsafeCell we wanted to match.

@RalfJung
Copy link
Member

RalfJung commented Feb 17, 2026

I only just reaized that SyncUnsafeCell has any bounds on its Sync impl -- given the name and intent, I find that very surprising. I would expect it to unconditionally implement Sync. In particular, I thought something like SyncUnsafeCell<*mut T> was an intended usecase.

@scottmcm
Copy link
Member

Pondering, without conclusions or advice:

  • This again makes me ponder the unsafe vs hold_my_beer distinction, since both unsafe static FOO and hold_my_beer static FOO potentially make sense, one like unsafe fields and one like "yes, this isn't sync, and I promise it's fine"
  • This would always be nicer, in a way, to keep using static mut FOO: T instead of static FOO: UnsafeCell<T>, right? Since then you get the type directly without needing to go through the unsafecell methods. (Said acknowledging my own inconsistency here of also liking the direction of moving towards deprecating static mut.)

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

Labels

I-lang-nominated Nominated for discussion during a lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang S-waiting-on-t-lang Status: Awaiting decision from T-lang T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

Comments