zdtm: add cow02 to cover a PROT_NONE COW root

Add a test for the SIGSEGV fixed by "mem: keep COW root VMAs writable
when premapping".

The parent reserves an address range with PROT_NONE and never touches
it, so the kernel does not account it (no "ac" flag in smaps). After
fork(), the child turns the same range into a writable mapping and fills
it with data. CRIU matches COW VMAs by start/end and flags but ignores
their protection bits, so the parent's PROT_NONE, not-accountable VMA
becomes the COW root of the child's data VMA.

On restore the child inherits the root's mapping. Without the fix that
mapping is PROT_NONE, so restoring the child's pages into it faults and
the test fails at restore; with the fix it passes.

Assisted-by: Claude Code:claude-opus-4-8
Signed-off-by: Radostin Stoyanov <rstoyanov@fedoraproject.org>
This commit is contained in:
Radostin Stoyanov 2026-06-10 15:40:39 +01:00 committed by Adrian Reber
parent 80bb396023
commit d3f88bb58d
2 changed files with 101 additions and 0 deletions

View file

@ -192,6 +192,7 @@ TST_NOFILE := \
socket-ext \
unhashed_proc \
cow00 \
cow02 \
child_opened_proc \
posix_timers \
sigpending \

100
test/zdtm/static/cow02.c Normal file
View file

@ -0,0 +1,100 @@
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>
#include "zdtmtst.h"
const char *test_doc = "Restore a COW child whose COW-root VMA is a PROT_NONE reservation";
const char *test_author = "Radostin Stoyanov <rstoyanov@fedoraproject.org>";
/*
* A parent reserves an address range with PROT_NONE and never touches it.
* Such a mapping has no "ac" flag in /proc/pid/smaps, so CRIU marks it
* VMA_AREA_NOT_ACCOUNTABLE. After fork() the child turns the very same range
* into a writable mapping and fills it with data.
*
* CRIU pairs VMAs that coincide by start/end and flags regardless of their
* protection bits (see check_cow_vmas()), so the parent's PROT_NONE,
* not-accountable VMA becomes the COW root of the child's data VMA.
*
* On restore, premap_private_vma() used to map a PROT_NONE & not-accountable
* COW root with PROT_NONE. The child then inherits that mapping (mremap) and
* restore_priv_vma_content() copies the child's pages into it -- writing to a
* PROT_NONE mapping faults and the restored task dies with SIGSEGV.
*
* The fix keeps a COW root writable; this test crashes at restore without it
* and passes with it.
*/
#define MEM_SIZE (32 * 4096)
int main(int argc, char **argv)
{
task_waiter_t lock;
void *mem;
pid_t pid;
int status;
uint32_t crc;
test_init(argc, argv);
task_waiter_init(&lock);
/* Parent: reserve the range and never write to it. */
mem = mmap(NULL, MEM_SIZE, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) {
pr_perror("Can't reserve memory");
return 1;
}
pid = test_fork();
if (pid < 0) {
pr_perror("Can't fork");
return 1;
}
if (pid == 0) {
/* Child: make the reservation writable and fill it. */
crc = ~0;
if (mprotect(mem, MEM_SIZE, PROT_READ | PROT_WRITE)) {
pr_perror("Child can't mprotect");
return 1;
}
datagen(mem, MEM_SIZE, &crc);
/* Ready for C/R. */
task_waiter_complete(&lock, 1);
/* Wait until the parent has been restored. */
task_waiter_wait4(&lock, 2);
crc = ~0;
if (datachk(mem, MEM_SIZE, &crc)) {
fail("Child data corrupted after restore");
return 1;
}
return 0;
}
/* Parent: wait for the child to populate its copy, stay PROT_NONE. */
task_waiter_wait4(&lock, 1);
test_daemon();
test_waitsig();
/* Restore succeeded -- let the child verify its content. */
task_waiter_complete(&lock, 2);
if (waitpid(pid, &status, 0) != pid) {
pr_perror("Can't wait for the child");
return 1;
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
fail("Child exited abnormally (status %d)", status);
return 1;
}
pass();
return 0;
}