On Wed, Jun 04, 2025 at 05:41:47PM +0200, Jann Horn wrote:
On Tue, Jun 3, 2025 at 10:32 PM Pedro Falcato pfalcato@suse.de wrote:
On Tue, Jun 03, 2025 at 08:21:02PM +0200, Jann Horn wrote:
When fork() encounters possibly-pinned pages, those pages are immediately copied instead of just marking PTEs to make CoW happen later. If the parent is multithreaded, this can cause the child to see memory contents that are inconsistent in multiple ways:
- We are copying the contents of a page with a memcpy() while userspace may be writing to it. This can cause the resulting data in the child to be inconsistent.
This is an interesting problem, but we'll get to it later.
- After we've copied this page, future writes to other pages may continue to be visible to the child while future writes to this page are no longer visible to the child.
Yes, and this is not fixable. It's also a problem for the regular write-protect pte path where inevitably only a part of the address space will be write-protected.
I don't understand what you mean by "inevitably only a part of the address space will be write-protected". Are you talking about how shared pages are kept shared between parent in child? Or are you talking about how there is a point in time at which part of the address space is write-protected while another part is not yet write-protected? In that case: Yes, that can happen, but that's not a problem.
This would only be fixable if e.g we suspended every thread on a multi-threaded fork.
No, I think it is fine to keep threads running in parallel on a multi-threaded fork as long as all the writes they do are guaranteed to also be observable in the child. Such writes are no different from writes performed before fork().
It would only get problematic if something in the parent first wrote to page A, which has already been copied to the child (so the child no longer sees the write) and then wrote to page B, which is CoWed (so the child would see the write). I prevent this scenario by effectively suspending the thread that tries to write to page A until the fork is over (by making it block on the mmap lock in the fault handling path).
Ah yes, I see my mistake - we write lock all VMAs as we dup them, so the problem I described can't happen. Thanks for the explanation :)