Vulnerability Report: Missing refcount_inc_not_zero + TOCTOU Race in Arc::into_unique_or_drop
Summary
rust/helpers/refcount.c omits refcount_inc_not_zero from its bindings to the kernel’s refcount_t API. This gap forces any Rust code that needs atomic “try-to-acquire” semantics to either bypass refcount protections entirely or use an inherently racy two-step check-then-increment. This directly enables a TOCTOU (Time-of-Check-Time-of-Use) race in Arc::into_unique_or_drop that leads to use-after-free, exploitable for kernel privilege escalation.
Affected Files
| File | Lines |
|---|---|
rust/helpers/refcount.c |
1–28 (missing refcount_inc_not_zero) |
rust/kernel/sync/refcount.rs |
28–31, 55–58 (new checks, set does not) |
rust/kernel/sync/arc.rs |
346–367 (into_unique_or_drop) |
Root Cause: Missing refcount_inc_not_zero Binding
rust/helpers/refcount.c exposes five helpers:
refcount_t rust_helper_REFCOUNT_INIT(int n)
void rust_helper_refcount_set(refcount_t *r, int n)
void rust_helper_refcount_inc(refcount_t *r)
void rust_helper_refcount_dec(refcount_t *r)
bool rust_helper_refcount_dec_and_test(refcount_t *r)
Critically absent: refcount_inc_not_zero (and refcount_inc_not_zero_acquire), which the kernel headers define at include/linux/refcount.h:333:
static inline __must_check bool refcount_inc_not_zero(refcount_t *r)
{
return __refcount_inc_not_zero(r, NULL); // atomic CAS: increment only if != 0
}
atomic_inc_not_zero is wrapped in rust/helpers/atomic.c:506, but that bypasses all of the refcount sanity-check infrastructure (overflow detection, use-after-free detection, saturation semantics).
Why This Matters
The kernel’s refcount_inc (which IS exposed) contains this contract:
Will WARN if the refcount is 0, as this represents a possible use-after-free condition.
It WARNs but still increments. Without refcount_inc_not_zero, Rust code that needs to conditionally acquire a reference (e.g., from an RCU-protected pointer, a weak reference, or a shared cache) cannot do so atomically at the refcount layer. The only options are:
- Use
refcount_incblindly — racy; can operate on freed memory - Use
as_atomic()+atomic_inc_not_zero— bypasses all refcount protections; disables overflow/UAF detection - Use external locks — negates the purpose of atomic reference counting
Exploitable Race: Arc::into_unique_or_drop
rust/kernel/sync/arc.rs:346–367:
pub fn into_unique_or_drop(this: Self) -> Option<Pin<UniqueArc<T>>> {
let this = ManuallyDrop::new(this);
let refcount = unsafe { &this.ptr.as_ref().refcount };
if refcount.dec_and_test() { // (A) refcount atomically: N→0, returns true
refcount.set(1); // (B) non-atomic write: 0→1
Some(Pin::from(UniqueArc { inner: ManuallyDrop::into_inner(this) }))
} else {
None
}
}
Between (A) and (B) there is a window where the refcount is 0 but the object has not been freed. Because refcount_inc_not_zero is unavailable, any concurrent C code or unsafe Rust code holding a raw pointer to the object is forced to use refcount_inc, which WARNs but still increments from 0→1.
Race Timeline (Use-After-Free)
Thread A (into_unique_or_drop) Thread B (C/unsafe code with raw ptr)
────────────────────────────────────── ──────────────────────────────────────
(A) dec_and_test() → refcount = 0
refcount_inc() → refcount 0→1 [WARN]
(B) refcount.set(1) → refcount = 1 (count is still 1; B's increment is lost)
returns UniqueArc (thinks sole owner) holds Arc-equivalent (refcount = 1)
UniqueArc dropped → dec_and_test()
→ refcount 1→0 → kfree(object) [object freed]
Thread B accesses freed object
→ USE-AFTER-FREE 🔴
refcount.set(1) overwrites the result of Thread B’s refcount_inc, silently discarding a live reference. Both threads now share ownership of an object with a refcount of 1. When the UniqueArc is dropped, the object is freed while Thread B still holds a dangling pointer.
Why refcount_inc_not_zero Would Prevent This
With refcount_inc_not_zero, Thread B’s atomic CAS would fail (returning false) when it observes refcount = 0, correctly signaling that the object is dead. Thread B would not increment, would not hold a dangling reference, and no use-after-free would occur. The absence of this binding is the direct enabler of the race.
Secondary Issue: Refcount::set Accepts Negative Values
rust/kernel/sync/refcount.rs:28–58:
pub fn new(value: i32) -> Self {
build_assert!(value >= 0, "initial value saturated"); // compile-time guard ✓
...
}
pub fn set(&self, value: i32) {
unsafe { bindings::refcount_set(self.as_ptr(), value) } // NO guard ✗
}
set(-1) can be called from safe Rust code (no unsafe block required). Negative values are treated as “saturated” by the kernel’s refcount implementation: the object can never be freed via normal decrement paths, causing permanent kernel memory leak. Repeated exploitation results in kernel OOM (denial of service).
Severity
| Property | Value |
|---|---|
| Class | Use-After-Free, Memory Leak |
| Impact | Kernel privilege escalation (UAF), DoS (OOM) |
| Exploitability | Race window between dec_and_test and set(1) |
| CVSS (estimated) | 7.8 High (UAF) / 5.5 Medium (OOM) |
Recommended Fix
1. Add the missing binding in rust/helpers/refcount.c:
bool rust_helper_refcount_inc_not_zero(refcount_t *r)
{
return refcount_inc_not_zero(r);
}
2. Expose it in rust/kernel/sync/refcount.rs:
#[inline]
#[must_use]
pub fn inc_not_zero(&self) -> bool {
// SAFETY: `self.as_ptr()` is valid.
unsafe { bindings::refcount_inc_not_zero(self.as_ptr()) }
}
3. Fix Arc::into_unique_or_drop to eliminate the zero-count window:
Replace the non-atomic set(1) resurrection with a compare_exchange loop that atomically moves the count from 0→1, failing if another thread has already modified it.
4. Add a bounds check in Refcount::set:
pub fn set(&self, value: i32) {
build_assert!(value >= 0, "value would saturate refcount");
unsafe { bindings::refcount_set(self.as_ptr(), value) }
}