fifo: don't reopen a writer-less non-blocking reader as a closed pipe - #3109
Conversation
5b7c9ff to
3dc3841
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## criu-dev #3109 +/- ##
===========================================
Coverage ? 57.75%
===========================================
Files ? 161
Lines ? 43951
Branches ? 9644
===========================================
Hits ? 25386
Misses ? 18326
Partials ? 239 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
The CentOS Stream 10 CI failures are unrelated. Looks like the iptables based network locking is not working. On CentOS Stream 10 we should switch to nftables. Let me do that. |
3dc3841 to
5066518
Compare
5066518 to
2fdfcc3
Compare
|
LGTM. Usually, we put test and criu changes into separate patches. |
2fdfcc3 to
6493dcf
Compare
I split it into two commits. |
|
Here is one more issue introduced by this patch. We need to distinguish two cases:
Here is a reproducer for the second case: |
|
@emirbuljubasic any update? |
Sorry for the delay, I'm on vacation. Will be back on this on monday. |
6493dcf to
f7b1eda
Compare
@avagin Thanks, I completely missed that case. I've tried to handle it by telling the two apart on dump. The only difference I could find is in poll(), so I poll the descriptor drained from the dumpee (its own struct file, so f_version is still intact) and store the answer in a new I wasn't sure about adding a field to the image just for this, so if there's a better place to get this from, or you'd prefer a different approach entirely, I'm happy to redo it. Your reproducer is in the test now, as a second fifo, since w_counter is per inode and the writer would otherwise hang up the first reader too. One thing I noticed while writing it: read() seems to return 0 in both cases, so I used the poll() check as the actual assertion. Let me know if I've got that wrong. Thank you everybody for being patient with me and for the informative comments. This is very much outside of my comfort zone. |
There was a problem hiding this comment.
Pull request overview
This PR fixes a FIFO restore corner case where an O_RDONLY|O_NONBLOCK reader that was originally opened without any writer can be restored in a way that makes poll() report POLLHUP continuously (causing busy-looping in epoll/poll driven daemons). It does so by recording, at dump time, whether the reader should “wait for a writer” (i.e., should not report POLLHUP) and then using that information to decide when to drop CRIU’s temporary fake writer during restore.
Changes:
- Add a new
fifo_entry.wait_writerimage field and populate it at dump time via apoll()probe on read-only FIFO fds. - Adjust FIFO restore logic to optionally close the temporary
O_RDWR“fake writer” before opening the realO_RDONLY|O_NONBLOCKfd (only for the “wait writer” state and only when no buffered FIFO data needs preserving). - Add a new ZDTM test (
fifo_ro_nonblock) covering both “wait for writer” and “closed pipe” FIFO poll semantics, and verifying pipe size restoration.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
test/zdtm/static/Makefile |
Adds the new static ZDTM test to the build/run set. |
test/zdtm/static/fifo_ro_nonblock.c |
New regression test for POLLHUP behavior across C/R for `O_RDONLY |
images/fifo.proto |
Extends FIFO image schema with wait_writer to preserve poll semantics. |
criu/fifo.c |
Dumps wait_writer state and updates restore to drop the fake writer early only when appropriate. |
Suppressed comments (1)
criu/fifo.c:188
- Same as above: add braces around the outer
ifguardingrestore_pipe_data()so the control flow is unambiguous and resistant to accidental future edits.
if (info->restore_data && drop_writer_early)
if (restore_pipe_data(CR_FD_FIFO_DATA, new_fifo, info->fe->pipe_id, pd_hash_fifo)) {
close(new_fifo);
new_fifo = -1;
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
do_open_fifo() opens a fake O_RDWR writer to unblock the restore, then opens the real descriptor while that writer is still present. For an O_RDONLY|O_NONBLOCK reader this caused it to return POLLHUP on every call indefinitely instead of blocking. In the case of epoll this would cause it to fire constantly, pinning the CPU at 100%. The kernel suppresses POLLHUP for a non-blocking read-only end only when it is opened with no writer around (fifo_open() latches w_counter into f_version), so no writer may be around when the real descriptor is opened. That is not always the right thing to do though: a read end whose writer has been opened and closed already is a closed pipe and has to keep reporting POLLHUP. Both ends look exactly the same in /proc, so tell them apart on dump by polling the descriptor drained from the dumpee -- POLLHUP there means the writer is gone for good -- and store the result in fifo_entry.wait_writer. Such an end is non-blocking, so it needs no fake writer to open in the first place: skip it entirely rather than open and close it. Opening one would also bump w_counter and hang up the ends restored before it, which matters as a fifo can have more than one read-only end waiting for a writer. Fifos with buffered data, blocking readers, write ends and the closed-pipe case keep the fake writer and are unchanged. Images without wait_writer keep the old behaviour as well. With no fake writer there is nothing else holding the pipe, so the pipe parameters are restored on the final descriptor instead. F_SETPIPE_SZ works on a read-only descriptor and the fifo carries no data here, so nothing else is lost. Assisted-by: Claude:claude-opus-5 Signed-off-by: Emir Buljubasic <emirbuljubasic329@gmail.com>
Check that both states of an O_RDONLY|O_NONBLOCK fifo end survive C/R: one opened with no writer around must not report POLLHUP, and one whose writer has been opened and closed must keep reporting it. Two fifos are needed as w_counter is per inode, so a writer on one end affects every reader of the same fifo. A second end waiting on the first fifo covers the case of more than one read-only end waiting for a writer, where restoring one of them must not hang up the others. Also check that the pipe size survives the restore, as the reader is reopened against a freshly created pipe object in the first case. Assisted-by: Claude:claude-opus-5 Signed-off-by: Emir Buljubasic <emirbuljubasic329@gmail.com>
f7b1eda to
68db6c7
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new wait_writer detection/metadata has a couple of correctness/semantic issues (notably POLLERR handling and documenting/recording wait_writer for blocking readers) that should be fixed to avoid misclassification.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
criu/fifo.c:101
- dump_one_fifo() sets fifo_entry.wait_writer for all O_RDONLY fds, but the surrounding comment describes the kernel behavior specifically for non-blocking readers. Since the restore-side logic only consults wait_writer for O_NONBLOCK anyway, limiting the field to O_RDONLY|O_NONBLOCK keeps the image semantics aligned with the documentation and avoids recording a potentially misleading value for blocking readers.
if ((p->flags & O_ACCMODE) == O_RDONLY) {
int wait_writer = fifo_wait_writer(lfd);
if (wait_writer < 0)
return -1;
e.has_wait_writer = true;
e.wait_writer = wait_writer;
}
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
| return -1; | ||
| } | ||
|
|
||
| return !(ret > 0 && (pfd.revents & POLLHUP)); |
| /* | ||
| * Set for a read-only end which has not seen a writer yet, i.e. | ||
| * the one poll() doesn't report POLLHUP for. | ||
| */ |
do_open_fifo()opens a temporaryO_RDWRfake writer to unblock the restore, then opens the real descriptor while that writer is still present. For anO_RDONLY|O_NONBLOCKreader, like OpenRC's/run/openrc/init.ctl, this leaves everypoll()returningPOLLHUPinstead of blocking, so an epoll loop spins at 100% CPU.As far as I can tell,
fifo_open()latchespipe->w_counterintofilp->f_versiononly when such an end is opened with no writer around, andpipe_poll()suppressesPOLLHUPwhile the two still match. So the fake writer has to be gone before the real descriptor is opened.Doing that unconditionally is wrong, as @avagin pointed out: an end whose writer has already been opened and closed is a closed pipe and must keep reporting
POLLHUP. The two states seem to differ nowhere but inpoll(), so this tells them apart on dump by polling the descriptor drained from the dumpee (it is the dumpee's ownstruct file, sof_versionis intact) and records the answer in a newfifo_entry.wait_writer. Images without the field keep the old behaviour.Per @rst0git: closing the fake writer drops the last reference to the pipe and takes the just restored pipe size with it, so the pipe parameters are restored on the final descriptor in that case.
The test covers both states. It needs two fifos, since
w_counteris per inode and a writer opened on one end hangs up every reader of that fifo. I checked that both assertions actually catch something: with the fake writer always kept the test fails on the waiting end, and with it always dropped it fails on @avagin's closed pipe reproducer.fifo,fifo_ro,fifo_wronlyandfifo-ghoststill pass.