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:

  1. Use refcount_inc blindly — racy; can operate on freed memory
  2. Use as_atomic() + atomic_inc_not_zero — bypasses all refcount protections; disables overflow/UAF detection
  3. 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)

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) }
}