The user-space layer that closes OneBarrier's fourth condition, Determinism. It intercepts an unmodified app's nondeterminism, records it, and replays it deterministically on a fresh instance after a crash. No kernel changes, no application changes (the SocksDirect lineage).
The other three conditions (Order, Barrier, Durability) come from the network,
and the engine that consumes them is in crates/onebarrier/. This layer is
separable on purpose: it needs no fabric and no special hardware, so everything
here runs on a commodity Linux box. See ../docs/how-it-works.md
for how the four fit together.
obpreload.c (an LD_PRELOAD shim, gcc -shared -fPIC -O2 -o libobpreload.so obpreload.c -ldl -lpthread) interposes the libc surface that carries
nondeterminism:
| Intercepted | Purpose |
|---|---|
accept/accept4, read/recv, close |
capture the request stream (the input) — OB_CAPTURE |
gettimeofday, clock_gettime, time, getrandom |
virtualize local nondeterminism |
The libOS is three composable LD_PRELOAD libraries (each independently useful):
| library | source | closes |
|---|---|---|
libobpreload.so |
obpreload.c |
socket capture + time (virtual clock / record-replay) |
librngdet.so |
rngdet.c |
RNG — raw getrandom syscall via seccomp-BPF |
libdetsched.so |
detsched.c |
thread scheduling — deterministic logical clocks |
Two virtualization strategies are provided, selected by environment variable:
- Record/replay (
OB_RECORD/OB_REPLAY) — log every nondeterministic result on the live run; return them in order on replay. Exact for request-driven time reads. - Virtual clock (
OB_VCLOCK) — time =base + ticks, whereticksadvance by a fixed delta on each socket read (a deterministic input event). Every time read is then count-independent, so timer-driven reads (redisserverCron, nginxngx_time_update) no longer desync replay. This is the general mechanism.
Record/replay alone replays time reads by sequence position. That is exact when
the app reads time once per request (Node's Date.now() aligned 8/8 across a
crash + 4 s gap). But timer-driven servers (redis serverCron) read time on
an internal timer whose firing count differs between record and replay, so the
sequential cursor desyncs — measured: redis TIME returned the recorded value
only sporadically.
The virtual clock closes that boundary. Because virtual time depends only on
the (deterministic) input-event count, not on how many times it is read, redis
TIME becomes byte-identical across recovery:
LIVE: 1782053569.269446 .270446 .271446 .272446 .273446 .274446
REPLAY: 1782053569.269446 .270446 .271446 .272446 .273446 .274446 (3 s real gap)
The replay's seconds ignore the real-time gap (it returns the live base, not
current time) and the microseconds advance deterministically. The virtual clock
is app-agnostic — it intercepts the libc time symbols for any binary (a
controlled test counted exactly 2,000,000 gettimeofday + 2,000,000
clock_gettime; the vDSO is not a blocker, since LD_PRELOAD overrides the
exported symbols).
ob-recover.sh <redis|memcached|nginx|node|all> [gap_s] runs the full cycle for
an unmodified app and includes a control that pins down causality:
- live — record under the virtual clock,
- crash (
kill -9) and waitgap_sof real wall-clock time, - replay — fresh instance, same persisted base → byte-identical to live,
- control — fresh instance with no virtual clock → real time, must differ.
Per-app time-dependent probes: redis TIME, memcached stats time, nginx Date:
header, Node Date.now(). The verdict requires BOTH replay == live AND
control != live, so a pass proves the determinism comes from the virtual clock,
not from a trivially-identical test.
gcc -shared -fPIC -O2 -o interpose/libobpreload.so interpose/obpreload.c -ldl -lpthread
cargo build --release -p onebarrier --bin ob-replay
bash interpose/ob-recover.sh all 3 # all 4 unmodified apps, one command
bash interpose/ob-recover.sh redis 3 # → "redis DETERMINISTIC ✅"
bash interpose/demo.sh # stock-redis record-replay STATE recoveryRepresentative run (all, 3 s gap, 2026-06-21):
redis live/replay 1782054868.424071 == 1782054868.424071 | control 1782054874.139431 ✅
memcached live/replay STAT time 1782054875 == 1782054875 | control STAT time 1782054880 ✅
nginx live/replay Date 15:14:41 GMT == 15:14:41 GMT | control Date 15:14:46 GMT ✅
node live/replay {"now":1782054887981} == ...887981 | control {"now":1782054893724} ✅
All five verified by ob-recover.sh (record → crash → real-time gap → replay →
diff), byte-identical across the gap:
| App | configuration | time-dependent probe | determinism |
|---|---|---|---|
engine apps (ob-kv/ob-mc/…) |
native | counter clock | deterministic by design ✅ |
| redis | single-threaded | TIME |
virtual clock → byte-identical ✅ |
| node | event loop | Date.now() |
virtual clock → byte-identical ✅ |
| memcached | -t 1 (single worker) |
stats time |
virtual clock → byte-identical ✅ |
| nginx | worker_processes 1 |
Date: header |
virtual clock → byte-identical ✅ |
(nginx is the strongest demonstration: the HTTP Date: header — formatted deep in
nginx's own code from its cached time — is frozen identically across a real-time
gap, e.g. Date: Sun, 21 Jun 2026 15:11:46 GMT on both the live and replayed
instance.)
State recovery (request-replay of SET/GET) is time-independent and works for
all KV apps regardless of the clock (the demo.sh stock-redis demonstration).
The virtual clock handles time; two more libOS components close the rest.
V8/Node, OpenSSL, and arc4random seed their PRNGs from the raw getrandom(2)
syscall, which bypasses LD_PRELOAD symbol interposition. rngdet.c installs a
seccomp-BPF user-notification filter that traps getrandom; a supervisor
thread fills the caller's buffer from a deterministic splitmix64 stream seeded from
a persisted seed (OB_VRAND=<file>), so live and replay observe the identical
random stream. Verified: a raw-getrandom C program returns identical bytes across
runs (520956f1…), where real entropy differs every run.
For V8, two more entropy sources must be pinned: ASLR addresses (setarch -R,
disable address-space randomization) and the RDRAND CPU instruction, which no
syscall trap can catch (OPENSSL_ia32cap='~0x4000000000000000:~0x0' makes OpenSSL
fall back to the trapped getrandom). With the full stack —
virtual clock + librngdet.so + ASLR-off + no-RDRAND — Node's Math.random() is
byte-identical across recovery:
live {"now":1782055180436,"rnd":0.27798545181677814}
replay {"now":1782055180436,"rnd":0.27798545181677814} (4 s real gap)
control{"now":1782055185175,"rnd":0.25929447455726007} (no stack — both differ)
ob-recover.sh applies this stack to every app automatically.
With >1 thread the OS chooses the order threads enter critical sections, so state
evolves nondeterministically. detsched.c interposes the pthread sync surface and
imposes Kendo-style deterministic logical clocks (Olszewski et al., ASPLOS'09):
a thread may take a top-level lock only when its (logical-clock, slot) is the
global minimum among active threads, then advances its clock. The order is a
function of the clocks, not OS timing, so the interleaving is identical every run.
Demonstrated by det-mt.sh:
- a 4-thread microbenchmark's critical-section order is identical across runs
(
order_hash=eef52ab…, 0 turn-relaxations) where it otherwise varies every run; - a condvar producer/consumer runs with no deadlock (parked threads leave the minimum; nested locks bypass the gate — both required to stay deadlock-free);
- it composes with a real multithreaded server:
memcached -t 4serves and stores under the scheduler.
Engineering notes that made it work on real binaries: gate only depth-0
acquisitions (nested locking deadlocked the naïve scheme); a bounded turn-wait
(OB_DETSCHED_SPIN, default 50000) keeps strict determinism for normal contention
but degrades to best-effort rather than hang on lock-heavy server init; and
pthread_cond_wait must be bound to its GLIBC_2.3.2 version via dlvsym
(plain dlsym returns the old compat shim and hangs threaded servers).
Scope (Kendo's domain): race-free programs whose threads make progress through sync operations. Pure-compute threads that never sync are out of scope (Kendo uses HW perf counters there).
redis 6 seeds its dict (SipHash) from /dev/urandom via fopen, which bypasses
both the getrandom seccomp trap and symbol interposition (glibc's internal
openat/read). So SPOP/SRANDMEMBER were nondeterministic across restarts. Fixed
by running redis in a private mount namespace with a deterministic file
bind-mounted over /dev/urandom — redis's RNG-derived state then recovers
byte-identically (popped/remaining set members identical live vs replay; a control
with the real device differs). librngdet.so also offers an optional syscall-level
openat redirect (OB_VRAND_OPENAT=1, via SECCOMP_IOCTL_NOTIF_ADDFD).
Replaying from process start costs O(all requests); a checkpoint bounds it to O(tail). Two mechanisms:
- App-native (works in-sandbox) —
ob-checkpoint-replay.sh: redis RDB snapshot- tail-replay, with the virtual clock resuming via
OB_VCLOCK_TICKS. Byte- identical state replaying 20 reqs vs 41 (2.05×).
- tail-replay, with the virtual clock resuming via
- General, any-binary —
ob-criu-kvm.sh: CRIU dumps the whole process (incl. the in-memory virtual clock), so restore needs no replay. The distro CRIU 3.16.1 segfaults on restore under kernel 6.8 (reproduces in a fresh KVM guest, so it's a CRIU-version issue, not the sandbox). The harness builds CRIU 3.19 and runs it in a KVM guest: checkpoint/restore of unmodified redis works, and the libOS virtual clock is preserved across C/R (RESULT-A and RESULT-B both PASS).ob-criu-checkpoint.shis the host-side attempt (documents the version blocker).
None of the above requires RDMA — the libOS is effort-gated, commodity-hardware work.