chunked: fix ETXTBSY race in FS_IOC_ENABLE_VERITY with ForkLock - #1030
chunked: fix ETXTBSY race in FS_IOC_ENABLE_VERITY with ForkLock#1030giuseppe wants to merge 3 commits into
Conversation
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
mtrmac
left a comment
There was a problem hiding this comment.
Just a quick look for now …
Preventing all forks for the duration of a hundreds-of-megabytes file operations is a bit of a cost, but without an O_CLOFORK it really might be the best we can do.
b17b9ff to
c7d3e1d
Compare
|
I have filed #1035 for unrelated the |
Luap99
left a comment
There was a problem hiding this comment.
Preventing all forks for the duration of a hundreds-of-megabytes file operations is a bit of a cost, but without an O_CLOFORK it really might be the best we can do.
Likely not a big deal for local podman, however the podman service and cri-o who do other work in parallel could need a long time until they fork some other process then if it writes a really big file.
I did not know go even offered ForkLock, but yeah I also see no way around using that then,
mtrmac
left a comment
There was a problem hiding this comment.
EnableVerity is also called in drivers/overlay; don’t we need something similar there as well?
It would probably be worth it to only prevent forks if FS verity is enabled; the PR does that in some but not all places. (A downside is that we might not notice all of the bad effects in non-FS-verity scenarios … but that’s also the upside.)
Hypothesizing about alternative approaches: The “unexpected fork” scenario presumably implies a future exec, I don’t think one can meaningfully for a multi-threaded Go program and expect it to continue. Could we first write all files, and then (in the same order) enable FS verity on all of them? That should mean a meaningful amount of time passes between closing the first writable FD (and risking a fork) and FS_IOC_ENABLE_VERITY, hoping that the exec happens in the meantime … except that might still be too quick for very small layers. Ugh… add a sleep?!
I don’t think ^^^ works well enough, hopefully others have better ideas.
To be explicit, do we have evidence that fork is the cause, or is it, at this point, a plausible hypothesis? (I generally agree that it is plausible, although I’m unsure what in the Podman process would be forking during a chunked pull).
The flake was only visible on podman-remote testing, aka podman system service does the pulling. bats tests run in parallel with other test cases. Other tests case runs a container == fork/exec various processes, conmon, netavark, etc... and podman system service handles all requests in parallel. So at least I think the story here checks out to the extend that we do only observe that failing on podman-remote. A |
|
you can use this reproducer to see I get: I'll take care of your other comments |
|
Sure I believe that a fork can trigger the error, it was more “is that how Podman is triggering it” — but Paul has a good explanation for that. |
Ensure writable fds are closed at exec(2) so they are not leaked to child processes. This narrows the fork race window for the ETXTBSY fix to only the fork-to-exec interval. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Concurrent fork(2) from other goroutines can duplicate a writable file descriptor. When the parent closes its copy, the forked child still holds a reference, so the kernel does not run __fput and inode->i_writecount remains elevated. FS_IOC_ENABLE_VERITY then fails with ETXTBSY because deny_write_access() sees a positive write count. Fix this by holding syscall.ForkLock.RLock() while a writable fd exists. Go's forkExec acquires the exclusive ForkLock.Lock(), so no fork(2) can proceed while we hold the read lock. The writable fd is closed and the lock released as early as possible: immediately after writing completes and a read-only fd has been obtained via /proc/self/fd. Closes: podman-container-tools/podman#28813 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Capture mkcomposefs output to a buffer first so no writable fd exists during cmd.Run(). Then write to the file and enable verity under ForkLock.RLock(), preventing concurrent fork(2) from duplicating the writable fd and causing ETXTBSY from FS_IOC_ENABLE_VERITY. Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
c7d3e1d to
910d3df
Compare
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
|
comments addressed and re-vendored in: podman-container-tools/podman#29309 |
|
@mtrmac any other blocker? |
Concurrent fork(2) from other goroutines can duplicate a writable file descriptor. When the parent closes its copy, the forked child still holds a reference, so the kernel does not run __fput and inode->i_writecount remains elevated. FS_IOC_ENABLE_VERITY then fails with ETXTBSY because deny_write_access() sees a positive write count.
Fix this by holding syscall.ForkLock.RLock() while a writable fd exists. Go's forkExec acquires the exclusive ForkLock.Lock(), so no fork(2) can proceed while we hold the read lock. The writable fd is closed and the lock released as early as possible: immediately after writing completes and a read-only fd has been obtained via /proc/self/fd.
Closes: podman-container-tools/podman#28813