# Impact of Static Variables on API Soundness

**URL:** <https://internals.rust-lang.org/t/impact-of-static-variables-on-api-soundness/24039>\
**Category:** Unsafe Code Guidelines\
**Created:** [February 26, 2026, 4:11pm UTC](https://internals.rust-lang.org/t/impact-of-static-variables-on-api-soundness/24039 "2026-02-26T16:11:58Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![hxuhack](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/hxuhack/32/13415_2.png) [@hxuhack](https://internals.rust-lang.org/u/hxuhack)\
**Post date:** [February 26, 2026, 4:11pm UTC](https://internals.rust-lang.org/t/impact-of-static-variables-on-api-soundness/24039/1 "2026-02-26T16:11:58Z")

</div>

I have a question regarding the policy for declaring API safety when it depends on the value of static variables. In the example below, the module `FooSys` contains a static variable `PTR` with internal mutability and two public APIs. Can the API `do_critical_task` be declared safe?

The function `do_critical_task` requires that `PTR` is either `null` or points to `CONFIG`. In the current module, this is guaranteed because `PTR` can only have these two states. However, future modifications to the module could introduce additional states or allow `PTR` to be changed, which could break this guarantee.

My specific questions are as follows:

1. In the current implementation, I believe `do_critical_task` can be declared safe. Is this correct?
2. If `PTR` remains private, but additional invalid states become possible, then `do_critical_task` cannot be declared safe, correct?
3. If `PTR` is made public (but not to other crates), meaning that additional invalid states could be introduced from outside the module, can `do_critical_task` still be declared safe?

```rust
mod FooSys {
    use std::{ptr, sync::atomic::{AtomicPtr, Ordering}};

    /// Static atomic pointer pointing to system configuration
    static PTR: AtomicPtr<u32> = AtomicPtr::new(ptr::null_mut());

    /// Initialize the system and set PTR to point to a valid static resource
    pub fn initialize_system() {
        static CONFIG: u32 = 42;
        PTR.store(&CONFIG as *const u32 as *mut u32, Ordering::SeqCst);
    }

    pub fn do_critical_task() {
        let ptr = PTR.load(Ordering::SeqCst);
        if !ptr.is_null() {
            unsafe { read_config(ptr); }
        }
    }

    // Safety: `ptr` must point to a valid configuration.
    unsafe fn read_config(ptr: *mut u32) {
       ...
    }
}

```

---

<div class="post-metadata">

**Author:** ![ia0](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/ia0/32/1396_2.png) [@ia0](https://internals.rust-lang.org/u/ia0)\
**Post date:** [February 26, 2026, 5:28pm UTC](https://internals.rust-lang.org/t/impact-of-static-variables-on-api-soundness/24039/2 "2026-02-26T17:28:35Z")

</div>

> [@hxuhack](#):
>
> In the current implementation, I believe `do_critical_task` can be declared safe. Is this correct?

Yes.

> [@hxuhack](#):
>
> If `PTR` remains private, but additional invalid states become possible, then `do_critical_task` cannot be declared safe, correct?

If `do_critical_task` is crate-public and those additional invalid states are reachable from crate-public safe functions, then yes, `do_critical_task` cannot be safe.

> [@hxuhack](#):
>
> If `PTR` is made public (but not to other crates), meaning that additional invalid states could be introduced from outside the module, can `do_critical_task` still be declared safe?

If the same conditions as above hold, then no, `do_critical_task` cannot be safe. However, if one of those conditions do not hold, then it can be declared safe. It's just a matter of style (crates with multiple contributors like the standard library may prefer to declare it `unsafe` to avoid misunderstandings, but simple crates with a single author might not bother in order to keep call sites readable).

---

<div class="post-metadata">

**Author:** ![jrose](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/jrose/32/9591_2.png) [@jrose](https://internals.rust-lang.org/u/jrose)\
**Post date:** [February 26, 2026, 5:59pm UTC](https://internals.rust-lang.org/t/impact-of-static-variables-on-api-soundness/24039/3 "2026-02-26T17:59:10Z")

</div>

For what it’s worth I think the “what about the future” part is a distraction for this question. Normally, yes, I’d be all for looking beyond today to make a better, more stable library for the future. But `unsafe` functions are those where the caller must guarantee some condition the compiler can’t check for them. Today, your function has _no_ preconditions from the perspective of an outside client. So what are they supposed to check?

Now consider that you add a new API that puts things in a bad state. (I’m intentionally being abstract because I don’t think involving a `static` is special; the pointer could just as easily be a member variable and you’d still be in this situation.) Now your original function has an uncheckable precondition, and it’s pretty easy to say it _should_ be marked `unsafe`. That’d be a breaking change if it wasn’t before, because old callers won’t compile.

However, even that’s a red herring. Suppose you documented the precondition as “you must not have called `foo()` before calling `critical()`”. And then in the _next_ release you add `bar()` that can _also_ put things in a bad state. Now your precondition has changed, but (at least with the way `unsafe` works today) there’s no source break. I’d argue that this is _still_ a semantically breaking change, because the client does not know they have to uphold the new precondition.

You could maybe argue that existing clients can’t be calling `bar()`, and thus no existing code is broken. That’s probably true for the time being. But without marking it as breaking, they won’t have a reason to see that the preconditions on `critical()` have changed. I mean, sure, you can read the release notes for non-breaking updates, but it’s not the same—I think a lot of times people would expect `cargo update && cargo test` to be sufficient.

So even if it’s not technically build-breaking, I think every change to a function’s unchecked preconditions should at least _default_ to being considered breaking, and therefore the idea of designing an `unsafe` function to be futureproof is not worth it.

---

<div class="post-metadata">

**Author:** ![programmerjake](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/programmerjake/32/5893_2.png) [@programmerjake](https://internals.rust-lang.org/u/programmerjake)\
**Post date:** [February 26, 2026, 11:16pm UTC](https://internals.rust-lang.org/t/impact-of-static-variables-on-api-soundness/24039/4 "2026-02-26T23:16:23Z")

</div>

Also, instead of marking your current API functions as `unsafe`, what you can do is mark whatever new functions you add that could break the invariants as `unsafe`, e.g.:

```rust
static ORIGINAL: u32 = 0;
static PTR: AtomicPtr<u32> = AtomicPtr::new(&raw const ORIGINAL as *mut u32);

pub fn existing_function() -> u32 {
    // Safety: PTR is known to contain a valid pointer and we used Acquire/Release correctly
    unsafe {
        *PTR.load(Ordering::Acquire)
    }
}

/// Safety: you must not pass in an invalid `v`
pub unsafe fn new_function(v: *const u32) {
    PTR.store(v as *mut u32, Ordering::Release);
}

```
