DrumsBench

Experiments / EXP-009

CRIU POSIX lock lost on a mapped file after restore, probe and patch candidate, unresolved

Isolate why a restored GNOME slot holds only one of its two Nautilus POSIX read locks at pre-resume, with a minimal probe that needs no desktop, and find out whether CRIU 4.3 can restore such a lock at all.

preliminaryA real observation that needs more runs, a positive control, or an independent check before it counts.
Date
2026-10-25
Hypothesis
CRIU 4.3 sets POSIX locks before it maps VMAs and then closes the VMA file descriptor, which drops the process's lock on any file it also maps.
Arms
drums-rootfs-criu
Hardware
authoring sandbox for the recorded probe runs (kernel reported as 6.18.44); the operator host has not run the probe per the branch
OS
Linux; CRIU 4.3 (tag v4.3, commit 2b7ac603), stock and patched builds
Workload
lockprobe: a child holding a POSIX read lock on a plain file A and on a file B it also mmaps; dump and restore with --file-locks and a pre-resume census
Code
claude/ecstatic-archimedes-456go6 at 7fd634c
Evidence
benchmark/sharedos_rootfs/LOCK_RESTORE_FAILURE_BRIEF.md
benchmark/sharedos_rootfs/lockprobe.py
benchmark/sharedos_rootfs/criu_patch/0001-restore-posix-locks-after-vma-fd-close.patch
Limitation
The stock "CONFIRMED mechanism" and the patched "both locks present" results are author-claimed sandbox text with no criu-phase-result.json on the branch; the prescribed order (stock on the operator host, patched, one GNOME gate) is not recorded as done, so the failure stays unresolved.

In one paragraph

When the custom rootfs slot (EXP-008) was finally dumped with --file-locks and restored with --root, CRIU said "Restore finished successfully. Tasks resumed." but our gate, which reads the restored tree's locks while every task is still stopped, found one of Nautilus's two SQLite locks missing: the lock on meta.db was there, the lock on meta.db-shm, a file Nautilus also memory-maps, was gone. The author wrote a tiny probe that reproduces the shape without GNOME, read the CRIU source for the ordering that could explain it, and produced a narrow patch candidate that sets POSIX locks after the VMA file descriptors are closed. The brief says the probe on stock CRIU 4.3 confirmed the loss and the patched build kept both locks, both in the authoring sandbox. Neither run has an artifact on the branch, the operator host has not run either, and the GNOME tree has not been restored with the patched binary. The failure is unresolved and the gate stays as it is.

Setup

Pins (from the brief)

What the probe does

Quoted from the brief, "What the probe does (CPU only; no mounts, no network, no model)":

A tiny child takes a POSIX read lock on file A (a plain file) and on file B (a file the child also mmaps through a second descriptor, like SQLite's *-shm). It is dumped with criu dump --file-locks and restored with criu restore --file-locks --restore-detached plus an --action-script whose pre-resume hook records every lock the restored tasks hold while they are still stopped.

In lockprobe.py the child locks A.db with fcntl.lockf(a, LOCK_SH, 510, 1073741826, 0) and B.db-shm with fcntl.lockf(b, LOCK_SH, 1, 128, 0) before mapping B.db-shm (4096 bytes, MAP_SHARED). Those ranges are the SQLite shared-lock bytes Nautilus held. Two phases: kernel (no CRIU, no root) and criu (root). The probe records the criu binary's resolved path, sha256 and --version. Its verdict strings are "CONFIRMED mechanism: the lock on the plain file survived and the lock on the file the task also maps is missing while the tasks are ..." and "NOT REPRODUCED with this criu binary: both locks are present before resume (compare criu_binary.sha256 with the stock run: this is the expected result of a patched build, and a failure to reproduce for a stock build)".

Commit 7fd634c fixed a probe defect: "criu dump kills the task, which stays a zombie holding its pid until the parent waits, so the restore failed with 'File exists'. The probe now waits (bounded) after a successful dump and stops owning the reaped pid."

observed (probe, stock CRIU 4.3)suspected ordering (source, v4.3)one process holdslock Aplain filelock Bfile it also mmapscriu dump --file-locksrecords 2 lockscriu restore --file-lockslog: applied 2pre-resume census, tasks stoppedA: presentB: ABSENTrestore failing; dump was completeprepare_file_locks()sets A and B on the fds of the taskopen_vmas()restorer closes the VMA fdPOSIX locks: per process + inode.closing any fd of that filedrops lock Bpatch candidate (unproven):set POSIX locks after VMA fd closecause not uniquely proven: the probeshows the effect, not the only causeUNRESOLVED: patch candidate, no recorded repair on the operator host
The suspected order inside stock CRIU 4.3's restore: file locks are applied, then VMAs are mapped through a separately opened descriptor which is closed afterwards; because a POSIX lock belongs to (process, inode), that close drops the lock on any file the task also maps. The probe shows the effect; it does not prove this is the only cause.

What ran

Per the branch's own text, all author-claimed with no result file on the branch:

Results

Expected vs actual, stock CRIU 4.3 (author-claimed, no artifact)

itemexpected for a faithful restoreactual
criu dump / criu restorerc 0 / rc 0rc 0 / rc 0
locks recorded at dump (dump.log LOCK flag:)22
locks the restore log reports applying (restore.log (posix)flag:)the same 22
before the dump (fdinfo of the tree)A and BA (510 bytes) and B (1 byte); 3 fdinfo entries for 2 host locks (B's lock appears on both descriptors that share the open file)
pre-resume census, A: A.db read lock, start 1073741826, len 510presentpresent
pre-resume census, B: B.db-shm read lock, start 128, len 1presentABSENT while all tasks are stopped
probe verdictboth presentCONFIRMED mechanism (A kept, B missing)
cleanupno leftoversno problems recorded

The brief's summary: "with stock CRIU 4.3, a restored process does not hold one of its two POSIX read locks while its tasks are still stopped at pre-resume. criu dump records both locks and the restore log reports applying both, but the lock on the file the process also maps is missing from the restored tasks. Restore is failing; that the dump recorded both locks does not make the restore correct."

Kernel-phase finding

Quoted from the brief: "Kernel phase (--phases kernel): closing a second descriptor of a file after mapping it drops the process's POSIX lock on that file, while an flock lock survives. This shows the kernel semantics (POSIX locks are per process and inode) that can produce the loss. It does not show that restore is correct: restore remains failing (B is absent before resume)."

Suspected ordering (cause not uniquely proven)

Quoted from the brief: "prepare_file_locks runs before open_vmas in restore_one_alive_task (cr-restore.c), and the VMA file descriptor is closed in the restorer (pie/restorer.c, VMA_CLOSE). The probe shows the effect (lock gone before resume). It does not by itself prove this ordering is the only cause." CANDIDATE.md adds the detail that files-reg.c open_filemap() opens the mapping's file through its own path, caches that descriptor across consecutive VMAs of the same file, and the restorer does sys_close(vma_entry->fd) right after the mmap, which predicts exactly "fd 8 kept, fd 10 lost while the tasks are stopped".

The patch candidate

criu_patch/0001-restore-posix-locks-after-vma-fd-close.patch (sha256 9fe4e36ca7735818b9181a0e589928819ee1f571f1bff140de694b4be63e6911), base v4.3 commit 2b7ac603. Mechanism: POSIX locks are no longer set in prepare_file_locks(); they are copied into task_restore_args.posix_locks of the owning task, and a new restore_posix_locks() in pie/restorer.c sets them with non-blocking F_SETLK after the VMA mapping stage, "after every VMA fd of this task has been closed". flock, OFD locks and leases are restored exactly as before. The --file-locks gate and the dump side are untouched; the image is not edited. It logs the same (posix)flag: ..., pid, fd, start, len line the harness gate parses, with a (set after VMA fd close) suffix. The patch touches criu/cr-restore.c, criu/file-lock.c, criu/include/file-lock.h, criu/include/restorer.h, criu/pie/restorer.c; we read the diff and it matches the README's description. "Nothing is uploaded or proposed upstream; the control (OCI reference, stock CRIU) is unchanged and keeps using the stock binary."

Author-claimed sandbox result (patch README, no artifact): "stock 016f89d4... -> CONFIRMED (the lock on the mapped file B was missing pre-resume, A present), patched a64bb2de... -> both locks present before resume, dumped == applied exactly ([1,0,pid,3,0x40000002,510], [1,0,pid,4,128,1])."

Declared limits: "multi-thread-group and nested pid-namespace trees have not been exercised by this patch; an fd that is itself closed by the task before resume has no lock to restore (same as stock). This is a candidate-arm repair, not a Drums result."

The untested order

The patch README prescribes, "Tests to run, in this order (operator, root, CPU only)":

  1. Control first, stock binary on the operator's host: expect CONFIRMED mechanism.
  2. Patched binary: expect NOT REPRODUCED with A and B both present pre-resume, dumped 2 / applied 2, cleanup_problems empty; the record names criu_binary.sha256.
  3. "Only if 2 converges: ONE full GNOME candidate gate with the new binary."

None of the three is recorded as done. The brief, written after the patch, still opens with "Restore is failing" and its commit calls itself "handoff only".

Failures and surprises

Why the gate is not relaxed

From CANDIDATE.md: "The gate is NOT relaxed. If the mechanism is confirmed, a lock-preserving restore of POSIX locks on mapped files is not possible with CRIU 4.3's ordering; the options (an evidence-named exception for locks on files the same task maps, or an upstream-side fix) are for review, not guessed here." And from the brief's gates: "until then no full run, and the lock gate is neither weakened nor skipped, the checkpoint images are not edited by hand, and --file-locks is not dropped." A restored desktop that silently lost a database lock is a correctness failure of the environment, which is exactly what the Drums comparison is supposed to catch.

Limitations

Reproduce

From the brief, on a root host, new evidence directory each time:

git worktree add /tmp/probe-7fd634c 7fd634c && cd /tmp/probe-7fd634c/benchmark
python3 -m sharedos_rootfs.lockprobe --evidence /var/tmp/lockprobe-kernel-NEW --phases kernel     # no root needed
STOCK_DIR=/path/to/dir-containing-the-verified-stock-criu
sudo env PATH="$STOCK_DIR:/usr/sbin:/usr/bin:/sbin:/bin" sh -c 'command -v criu && criu --version && sha256sum "$(command -v criu)"'
sudo env PATH="$STOCK_DIR:/usr/sbin:/usr/bin:/sbin:/bin" python3 -m sharedos_rootfs.lockprobe --evidence /var/tmp/lockprobe-criu-NEW --phases criu
cat /var/tmp/lockprobe-criu-NEW/criu-lock/criu-phase-result.json   # dump_rc, restore_rc, dumped_log_locks, applied_log_locks, pre_resume, verdict

Then the patched build (git clone --branch v4.3 ... && git apply .../0001-restore-posix-locks-after-vma-fd-close.patch && make -j criu), the probe again with --criu /path/patched/criu, and only if that converges, one GNOME candidate gate. Publishing the three result files would close this record.

Sources