Skip to content
Haiku OSDeep Dive Published Updated 6 min readViews unavailable

Haiku Kernel rw_locks: Shared Reads, Exclusive Writes, and Teardown

Trace Haiku's private kernel rw_lock implementation for reader and writer handoff, name ownership, balanced use, and safe teardown.

Haiku’s kernel rw_lock is a reader-writer synchronization primitive in the private kernel headers. It permits concurrent readers while excluding readers and writers during an exclusive write section. The interesting operational details are not a stable userland API promise: the header lives under headers/private/kernel, and the queueing, timeout, debug, and teardown behavior discussed here is based on a pinned Haiku source revision. Kernel code should re-check the target tree rather than infer that these internals are a public compatibility contract.

The central design rule remains simple: protect a predicate or object state, not an arbitrary pointer lifetime. Readers should copy the state they need while holding the read lock; writers should update the invariant while holding the write lock; object teardown must stop new callers and wait for every existing reader and writer to leave before storage is reclaimed.

The interface and its name-lifetime contract

The private header declares rw_lock_init, rw_lock_init_etc, rw_lock_destroy, rw_lock_write_lock, and read/write unlock helpers. In this revision, the inline rw_lock_read_lock path returns a status_t, as do the contended write-lock calls; check the status instead of assuming acquisition succeeded. The header also defines a static RW_LOCK_INITIALIZER(name) form for statically initialized locks.

Names are a small but concrete lifetime boundary. The header comments that rw_lock_init() does not clone or free the supplied name at destroy time. rw_lock_init_etc() clones the name only when RW_LOCK_FLAG_CLONE_NAME is set. A stack-local or temporary string is therefore unsafe for ordinary initialization if diagnostics can later read it. Use a static-lifetime name or the explicit clone flag when the caller cannot guarantee that storage remains valid.

An illustrative kernel-side critical section looks like this. The containing object’s owner initializes the lock before publishing the object; this operation returns immediately on an acquisition error instead of attempting another lock operation or destroying a lock that may be shutting down:

status_t refresh_cache(cache_state* cache)
{
	status_t status = rw_lock_read_lock(&cache->lock);
	if (status != B_OK)
		return status; // Stop using this object; its owner handles teardown.

	uint64 generation = cache->generation;
	Snapshot local = cache->snapshot;
	rw_lock_read_unlock(&cache->lock);
	consume_snapshot(local, generation);

	status = rw_lock_write_lock(&cache->lock);
	if (status != B_OK)
		return status; // Do not retry against an object being dismantled.

	update_cache_invariant(cache);
	rw_lock_write_unlock(&cache->lock);
	return B_OK;
}

The type names and calls are from Haiku’s private kernel API, not code to compile in a normal application; cache_state is illustrative and its lock is initialized before publication. The snippet assumes Snapshot is safe to copy while protected; a pointer copied out of the lock still needs a separate reference or ownership protocol. rw_lock_destroy() belongs only in the owner’s teardown path, after admission is closed and holders and waiters are drained; an acquisition error is not permission for this operation to destroy or otherwise reuse the lock.

Reader and writer handoff in the pinned implementation

In the cited lock.cpp, an uncontended reader first updates a counter atomically. A writer marks its claim in the same counter and takes the fast path only if there are no existing readers or writers. Under contention, callers enter a waiter queue. When the lock becomes available, the implementation gives the writer at the head of the queue the lock if no active or pending readers remain; if readers are at the head, it releases the queued reader batch together. This source-level handoff policy is useful when diagnosing a particular Haiku build, but it is not a promise of FIFO service, bounded waiting, or a fairness guarantee for every future revision.

The implementation comments on a subtle deadlock: taking another read lock while already holding a read lock can deadlock if a writer is waiting. The associated ASSERT_UNLOCKED_RW_LOCK assertion is only active when the debug-lock configuration enables it; it does not prevent the deadlock in ordinary builds. Keep lock acquisition balanced and shallow. Do not assume that upgrading a read lock to a write lock is atomic, or that dropping a read lock and later acquiring a write lock preserves the state you observed. If an update is needed, release the read lock, acquire the writer lock, then re-check the predicate before modifying anything.

Haiku’s BFS source has a particularly instructive internal comment: its asynchronous bfs_io path depends on read locks being implicitly transferable across threads, and a debug-lock mode disables that path because it cannot support that assumption. This is an implementation-specific filesystem integration detail, not a general recommendation to unlock a lock from a different thread. Most callers should acquire and release in one clearly owned control flow and must not depend on undocumented transfer behavior.

Teardown is an owner protocol, not a destroy call

rw_lock_destroy() cannot make it safe to free an object that other threads can still reach. In the pinned implementation it removes queued waiters and wakes them with B_ERROR. Under KDEBUG, destroying an in-use lock when the caller is not its current writer triggers a diagnostic panic path; verify the owning subsystem’s required exclusive-ownership point instead of calling destroy from an arbitrary worker. Waking a waiter does not revoke a pointer already held by another thread, join workers, or prevent a new caller from arriving. The owner must first close admission, signal or cancel workers through its own protocol, and ensure no other holder, waiter, or asynchronous completion can still touch the containing object. Only then should it destroy the lock at the call site’s supported ownership point and eventually reclaim storage.

If a lock call returns an error during teardown, its caller must stop using the protected object and propagate or handle that status. Do not spin and retry against memory whose owner may be dismantling it. Keep the object alive until all waiters have observed the shutdown result and all holders have released their lock.

For a source review, pin the exact Haiku commit and inspect the header, implementation, and call site together. Exercise simultaneous readers, a writer arriving behind them, readers arriving after the writer queues, timeout/error paths, and shutdown with waiters. Run the kernel’s supported debug configuration when possible; static inspection alone cannot prove the caller’s lifetime protocol. The durable lesson is to distinguish API shape from implementation behavior, and synchronization from object ownership.

When a shutdown test fails, log the lock identity, acquisition result, holder/waiter transition, and the owning object’s state from a path safe for the kernel subsystem. Avoid adding verbose logging while interrupts are constrained or while holding another lock without first checking that path’s lock-order rules. A useful stress test makes readers pause at a controlled point, queues a writer, starts a later reader, and then releases each phase while asserting that protected invariants remain valid. Repeat with teardown initiated only after admission is closed; a clean test under one timing does not replace the lifetime proof.

Related:

Sources:

Comments