lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <CAGsJ_4zMNaNjRXbtDD6xYfDhckcDCnTvv+4-yB1xpuYcur=fyw@mail.gmail.com>
Date: Fri, 23 May 2025 11:23:31 +1200
From: Barry Song <21cnbao@...il.com>
To: Peter Xu <peterx@...hat.com>, David Hildenbrand <david@...hat.com>, 
	Suren Baghdasaryan <surenb@...gle.com>, Lokesh Gidra <lokeshgidra@...gle.com>, 
	Andrea Arcangeli <aarcange@...hat.com>
Cc: Andrew Morton <akpm@...ux-foundation.org>, Linux-MM <linux-mm@...ck.org>, 
	Kairui Song <ryncsn@...il.com>, LKML <linux-kernel@...r.kernel.org>
Subject: [BUG]userfaultfd_move fails to move a folio when swap-in occurs
 concurrently with swap-out

Hi All,

I'm encountering another bug that can be easily reproduced using the small
program below[1], which performs swap-out and swap-in in parallel.

The issue occurs when a folio is being swapped out while it is accessed
concurrently. In this case, do_swap_page() handles the access. However,
because the folio is under writeback, do_swap_page() completely removes
its exclusive attribute.

do_swap_page:
               } else if (exclusive && folio_test_writeback(folio) &&
                          data_race(si->flags & SWP_STABLE_WRITES)) {
                        ...
                        exclusive = false;

As a result, userfaultfd_move() will return -EBUSY, even though the
folio is not shared and is in fact exclusively owned.

                        folio = vm_normal_folio(src_vma, src_addr,
orig_src_pte);
                        if (!folio || !PageAnonExclusive(&folio->page)) {
                                spin_unlock(src_ptl);
+                               pr_err("%s %d folio:%lx exclusive:%d
swapcache:%d\n",
+                                       __func__, __LINE__, folio,
PageAnonExclusive(&folio->page),
+                                       folio_test_swapcache(folio));
                                err = -EBUSY;
                                goto out;
                        }

I understand that shared folios should not be moved. However, in this
case, the folio is not shared, yet its exclusive flag is not set.

Therefore, I believe PageAnonExclusive is not a reliable indicator of
whether a folio is truly exclusive to a process.

The kernel log output is shown below:
[   23.009516] move_pages_pte 1285 folio:fffffdffc01bba40 exclusive:0
swapcache:1

I'm still struggling to find a real fix; it seems quite challenging.
Please let me know if you have any ideas. In any case It seems
userspace should fall back to userfaultfd_copy.



[1] The small program:

//Just in a couple of seconds, we are running into
//"UFFDIO_MOVE: Device or resource busy"

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <linux/userfaultfd.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <errno.h>

#define PAGE_SIZE 4096
#define REGION_SIZE (512 * 1024)

#ifndef UFFDIO_MOVE
struct uffdio_move {
    __u64 dst;
    __u64 src;
    __u64 len;
    #define UFFDIO_MOVE_MODE_DONTWAKE        ((__u64)1<<0)
    #define UFFDIO_MOVE_MODE_ALLOW_SRC_HOLES ((__u64)1<<1)
    __u64 mode;
    __s64 move;
};

#define _UFFDIO_MOVE  (0x05)
#define UFFDIO_MOVE   _IOWR(UFFDIO, _UFFDIO_MOVE, struct uffdio_move)
#endif


void *src, *dst;
int uffd;

void *madvise_thread(void *arg) {
    for (size_t i = 0; i < REGION_SIZE; i += PAGE_SIZE) {
    madvise(src + i, PAGE_SIZE, MADV_PAGEOUT);
    usleep(100);
    }
    return NULL;
}

void *swapin_thread(void *arg) {
    volatile char dummy;
    for (size_t i = 0; i < REGION_SIZE; i += PAGE_SIZE) {
        dummy = ((char *)src)[i];
        usleep(100);
    }
    return NULL;
}


void *fault_handler_thread(void *arg) {

    struct uffd_msg msg;
    struct uffdio_move move;
    struct pollfd pollfd = { .fd = uffd, .events = POLLIN };
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

    while (1) {
        if (poll(&pollfd, 1, -1) == -1) {
            perror("poll");
            exit(EXIT_FAILURE);
        }

        if (read(uffd, &msg, sizeof(msg)) <= 0) {
            perror("read");
            exit(EXIT_FAILURE);
        }


        if (msg.event != UFFD_EVENT_PAGEFAULT) {
            fprintf(stderr, "Unexpected event\n");
            exit(EXIT_FAILURE);
        }

        move.src = (unsigned long)src + (msg.arg.pagefault.address -
(unsigned long)dst);
        move.dst = msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
        move.len = PAGE_SIZE;
        move.mode = 0;

        if (ioctl(uffd, UFFDIO_MOVE, &move) == -1) {
            perror("UFFDIO_MOVE");
            exit(EXIT_FAILURE);
        }
    }
    return NULL;
}

int main() {
again:
    pthread_t thr, madv_thr, swapin_thr;
    struct uffdio_api uffdio_api = { .api = UFFD_API, .features = 0 };
    struct uffdio_register uffdio_register;

    src = mmap(NULL, REGION_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE
| MAP_ANONYMOUS, -1, 0);

    if (src == MAP_FAILED) {
        perror("mmap src");
        exit(EXIT_FAILURE);
    }

    memset(src, 1, REGION_SIZE);

    dst = mmap(NULL, REGION_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE
| MAP_ANONYMOUS, -1, 0);

    if (dst == MAP_FAILED) {
        perror("mmap dst");
        exit(EXIT_FAILURE);
    }


    uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1) {
        perror("userfaultfd");
        exit(EXIT_FAILURE);
    }


    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) {
        perror("UFFDIO_API");
        exit(EXIT_FAILURE);
    }

    uffdio_register.range.start = (unsigned long)dst;
    uffdio_register.range.len = REGION_SIZE;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;

    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) {
        perror("UFFDIO_REGISTER");
        exit(EXIT_FAILURE);

    }

    if (pthread_create(&madv_thr, NULL, madvise_thread, NULL) != 0) {
        perror("pthread_create madvise_thread");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&swapin_thr, NULL, swapin_thread, NULL) != 0) {
        perror("pthread_create swapin_thread");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&thr, NULL, fault_handler_thread, NULL) != 0) {
        perror("pthread_create fault_handler_thread");
        exit(EXIT_FAILURE);
    }

    for (size_t i = 0; i < REGION_SIZE; i += PAGE_SIZE) {
        char val = ((char *)dst)[i];
        printf("Accessing dst at offset %zu, value: %d\n", i, val);
    }

    pthread_join(madv_thr, NULL);
    pthread_join(swapin_thr, NULL);
    pthread_cancel(thr);
    pthread_join(thr, NULL);
    munmap(src, REGION_SIZE);
    munmap(dst, REGION_SIZE);
    close(uffd);
    goto again;

    return 0;
}

Thanks
Barry

Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ