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)
- "CRIU 4.3, upstream tag
v4.3, commit2b7ac603cda31d015d91c856c34c557cb2300e7d, stock unmodified build." - "Probe: this repository at commit
7fd634c(benchmark/sharedos_rootfs/lockprobe.pywith thelocks.pyof the same commit; the dumped child is reaped before the restore). Run from a checkout of exactly that commit so later edits on the branch cannot change what is measured." - Upstream comparison: public HEAD
695e88awas compared against v4.3;file-lock.c,files-reg.candpie/restorer.care byte-identical andcr-restore.cdiffers only by an unrelated warning change. "Limits: this was a source comparison only; HEAD was not rebuilt or run, so its behaviour is not measured."
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 withcriu dump --file-locksand restored withcriu restore --file-locks --restore-detachedplus an--action-scriptwhosepre-resumehook 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."
What ran
Per the branch's own text, all author-claimed with no result file on the branch:
- The GNOME run that exposed the failure (EXP-008, step 8, commit
fd39055): restore log applied both locks, pre-resume hook saw one. - The probe's
kernelphase in the authoring sandbox (kernel 6.18.44, author-reported). - The probe's
criuphase with a stock CRIU 4.3 build (binary sha256 prefix016f89d4) and with the patched build (a64bb2de), both in the authoring sandbox, "NOT the operator's host, kernel differs" (patch README).
Results
Expected vs actual, stock CRIU 4.3 (author-claimed, no artifact)
| item | expected for a faithful restore | actual |
|---|---|---|
criu dump / criu restore | rc 0 / rc 0 | rc 0 / rc 0 |
locks recorded at dump (dump.log LOCK flag:) | 2 | 2 |
locks the restore log reports applying (restore.log (posix)flag:) | the same 2 | 2 |
| before the dump (fdinfo of the tree) | A and B | A (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 510 | present | present |
pre-resume census, B: B.db-shm read lock, start 128, len 1 | present | ABSENT while all tasks are stopped |
| probe verdict | both present | CONFIRMED mechanism (A kept, B missing) |
| cleanup | no leftovers | no 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)":
- Control first, stock binary on the operator's host: expect
CONFIRMED mechanism. - Patched binary: expect
NOT REPRODUCEDwith A and B both present pre-resume, dumped 2 / applied 2,cleanup_problemsempty; the record namescriu_binary.sha256. - "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
- The first lock gate (post-resume census) was wrong for a different reason: SQLite shared locks are held only during a read statement, so a resumed Nautilus can release them legitimately within milliseconds. That gate was moved to CRIU's
pre-resumehook, where tasks are stopped, and it still failed. That second failure is the real one. - The dump side is not at fault: both locks are in
filelocks.imgand both appear in the restore log as applied. The loss happens after the log line, before resume. - Where the stock result was obtained is only stated in the patch README ("authoring environment, NOT the operator's host, kernel differs"); the brief's commands section is written for the operator's root host and warns that
sudoresetsPATH, so thecriuundersudomay not be the binary checked as a user.
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
- No
criu-phase-result.json, nokernel-phase output, no full binary hashes (only 8-character prefixes) on the branch. - Sandbox only; the operator host (different kernel) has not run the probe per the branch.
- The source ordering is a well-supported hypothesis, not a proven unique cause.
- The patch has not been tried on the GNOME tree, on multi-threaded tasks or on nested pid namespaces.
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
benchmark/sharedos_rootfs/LOCK_RESTORE_FAILURE_BRIEF.md(and the.txttwin with the same content)benchmark/sharedos_rootfs/CANDIDATE.md, section 3bbenchmark/sharedos_rootfs/lockprobe.py,benchmark/sharedos_rootfs/locks.pybenchmark/sharedos_rootfs/criu_patch/README.md,benchmark/sharedos_rootfs/criu_patch/0001-restore-posix-locks-after-vma-fd-close.patch- Commits
a84dde4,fd39055,b16a238,3ef865a,7fd634c,d86c223,87a3cd7,6fb6566,f26260f,91988fc(2026-10-25)