// https://syzkaller.appspot.com/bug?id=23e3f76d4a09fecb55816c5be67cbd43f10b3eed #define _GNU_SOURCE // Copyright 2026 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. // IMPORTANT: Do not copy the macros or definitions below directly into your reproducer. // Instead, add the following line to your reproducer: // #include "race_toolkit.h" // --- Race Condition Toolkit --- // Macros and snippets for CPU pinning, memory barriers, and userfaultfd. #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Unbuffered I/O: Ensure logs are written immediately. #define SETUP_UNBUFFERED_IO() setvbuf(stdout, NULL, _IONBF, 0) // CPU Pinning: Pin the current thread to a specific CPU core. #define PIN_TO_CPU(cpu) \ do { \ cpu_set_t mask; \ CPU_ZERO(&mask); \ CPU_SET(cpu, &mask); \ if (sched_setaffinity(0, sizeof(mask), &mask) == -1) { \ perror("sched_setaffinity"); \ } \ } while (0) // Memory Barrier: Ensure memory ordering. #define MB() __atomic_thread_fence(__ATOMIC_SEQ_CST) // Spin-wait Barrier: Wait until a memory location has a specific value. // Best for tight race windows (low latency, no context switches). #define WAIT_ON(addr, val) \ do { \ while (__atomic_load_n(addr, __ATOMIC_ACQUIRE) != (val)) \ ; \ } while (0) // Signal: Set a memory location to a specific value to release a WAIT_ON. #define SIGNAL(addr, val) __atomic_store_n(addr, val, __ATOMIC_RELEASE) // --- Timing Primitives --- // Robust timing loops in VM environments (using CLOCK_MONOTONIC to avoid time(NULL) jumps). static inline double timer_elapsed_sec(struct timespec* start) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) { perror("clock_gettime(CLOCK_MONOTONIC) elapsed"); exit(1); } return (double)(now.tv_sec - start->tv_sec) + (double)(now.tv_nsec - start->tv_nsec) / 1e9; } // Initialize a monotonic timer variable. #define TIMER_START(t) \ struct timespec t; \ if (clock_gettime(CLOCK_MONOTONIC, &t) == -1) { \ perror("clock_gettime(CLOCK_MONOTONIC) start"); \ exit(1); \ } // Check if the elapsed time since 't' is less than 'sec' seconds. #define TIMER_NOT_EXPIRED(t, sec) (timer_elapsed_sec(&(t)) < (double)(sec)) // Futex-based Event: Shared with syzkaller executor. // Best for general synchronization or longer waits to save CPU. typedef struct { int state; } event_t; static void event_init(event_t* ev) { ev->state = 0; } static void event_reset(event_t* ev) { ev->state = 0; } static void event_set(event_t* ev) { if (__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE)) { fprintf(stderr, "event already set\n"); exit(1); } __atomic_store_n(&ev->state, 1, __ATOMIC_RELEASE); syscall(SYS_futex, &ev->state, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1000000); } static void event_wait(event_t* ev) { while (!__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE)) syscall(SYS_futex, &ev->state, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 0, 0); } // userfaultfd setup: Register a memory range for page fault handling. static int setup_uffd(void* addr, size_t len) { int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1) return -1; struct uffdio_api api = {.api = UFFD_API, .features = 0}; if (ioctl(uffd, UFFDIO_API, &api) == -1) { close(uffd); return -1; } struct uffdio_register reg = { .range = {.start = (uintptr_t)addr, .len = len}, .mode = UFFDIO_REGISTER_MODE_MISSING}; if (ioctl(uffd, UFFDIO_REGISTER, ®) == -1) { close(uffd); return -1; } return uffd; } // --- Guidance on Usage --- // 1. Use WAIT_ON/SIGNAL for tight race conditions to avoid scheduling overhead. // 2. Use event_t (futexes) for general coordination or when waiting for longer periods. // 3. Always use PIN_TO_CPU to increase race probability on multi-core systems. // 4. Use setup_uffd to register a memory range for page fault handling. This allows you to // pause a thread accessing that memory until you handle the fault, creating a reliable // and controllable race window. // 5. Call SETUP_UNBUFFERED_IO() at the start of main() to ensure that logs are printed // immediately. This is essential for understanding the exact interleaving of events // when debugging race conditions. // 6. For timing-based loops (e.g., running a race for 10 seconds), do NOT use time(NULL) // or loops relying on real-time clocks, as VM clocks are highly unreliable and can fail or drift. // Instead, use the robust monotonic timing primitives TIMER_START and TIMER_NOT_EXPIRED: // TIMER_START(start); // while (TIMER_NOT_EXPIRED(start, 10.0)) { // // Your race logic here // } #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef MFD_HUGETLB #define MFD_HUGETLB 0x0004U #endif #define HPAGE_SIZE (2 * 1024 * 1024UL) #define NUM_PAGES 4 #define FILE_SIZE (HPAGE_SIZE * NUM_PAGES) #define NUM_PRIV 4 #define NUM_WORKERS 8 static char *shared_map = NULL; static char *priv_map[NUM_PRIV]; static volatile int stop = 0; static __thread sigjmp_buf jmpbuf; static __thread volatile sig_atomic_t can_jump = 0; static void sigbus_handler(int sig) { (void)sig; if (can_jump) { siglongjmp(jmpbuf, 1); } _exit(135); } static void *worker_routine(void *arg) { long id = (long)arg; int num_cpus = sysconf(_SC_NPROCESSORS_ONLN); if (num_cpus < 1) num_cpus = 1; PIN_TO_CPU((int)((id + 1) % num_cpus)); unsigned char vec[(FILE_SIZE / 4096) + 1]; while (!__atomic_load_n(&stop, __ATOMIC_ACQUIRE)) { int priv_idx = (int)(id % NUM_PRIV); if (id < 4) { /* Threads 0..3: rapid mprotect toggling on private VMA. * Hugetlb change_protection acquires hugetlb_vma_lock_write (resv_map->rw_sema) * before i_mmap_lock_write, creating lock contention with ftruncate. */ mprotect(priv_map[priv_idx], FILE_SIZE, PROT_READ); mprotect(priv_map[priv_idx], FILE_SIZE, PROT_READ | PROT_WRITE); } else if (id < 6) { /* Threads 4..5: rapid mincore queries on all private VMAs. * walk_hugetlb_range holds hugetlb_vma_lock_read (resv_map->rw_sema). */ for (int k = 0; k < NUM_PRIV; k++) { mincore(priv_map[k], FILE_SIZE, vec); } } else if (id == 6) { /* Thread 6: madvise MADV_DONTNEED + re-read */ for (int k = 0; k < NUM_PRIV; k++) { madvise(priv_map[k], FILE_SIZE, MADV_DONTNEED); if (sigsetjmp(jmpbuf, 1) == 0) { can_jump = 1; for (size_t p = 0; p < NUM_PAGES; p++) { volatile char c = priv_map[k][p * HPAGE_SIZE]; (void)c; } } can_jump = 0; } } else { /* Thread 7: continuous background reader to ensure PTEs are present */ for (int k = 0; k < NUM_PRIV; k++) { if (sigsetjmp(jmpbuf, 1) == 0) { can_jump = 1; for (size_t p = 0; p < NUM_PAGES; p++) { volatile char c = priv_map[k][p * HPAGE_SIZE]; (void)c; } } can_jump = 0; } } } return NULL; } int main(void) { setvbuf(stdout, NULL, _IONBF, 0); printf("[*] Starting reproducer for refcount bug in remove_inode_hugepages...\n"); /* Step 1: Ensure hugepage pool availability */ int nr_fd = open("/proc/sys/vm/nr_hugepages", O_RDWR); if (nr_fd < 0) { printf("[-] Failed to open /proc/sys/vm/nr_hugepages: %s\n", strerror(errno)); exit(1); } printf("[+] open /proc/sys/vm/nr_hugepages successful.\n"); const char *val = "40\n"; ssize_t w = write(nr_fd, val, strlen(val)); if (w < 0) { printf("[-] Failed to write to /proc/sys/vm/nr_hugepages: %s\n", strerror(errno)); exit(1); } printf("[+] write to /proc/sys/vm/nr_hugepages successful.\n"); int res = close(nr_fd); if (res < 0) { printf("[-] Failed to close /proc/sys/vm/nr_hugepages: %s\n", strerror(errno)); exit(1); } printf("[+] close /proc/sys/vm/nr_hugepages successful.\n"); /* Step 2: Install SIGBUS handler for worker threads */ struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = sigbus_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; res = sigaction(SIGBUS, &sa, NULL); if (res < 0) { printf("[-] Failed to install SIGBUS handler: %s\n", strerror(errno)); exit(1); } printf("[+] SIGBUS handler installed successfully.\n"); /* Step 3: Create hugetlbfs file descriptor */ int huge_fd = memfd_create("huge_repro", MFD_HUGETLB); if (huge_fd < 0) { printf("[-] Failed to memfd_create with MFD_HUGETLB: %s\n", strerror(errno)); exit(1); } printf("[+] memfd_create with MFD_HUGETLB successful.\n"); /* Step 4: Truncate file to initial size */ res = ftruncate(huge_fd, FILE_SIZE); if (res < 0) { printf("[-] Failed to ftruncate hugetlb file: %s\n", strerror(errno)); exit(1); } printf("[+] Initial ftruncate to %zu bytes successful.\n", FILE_SIZE); /* Step 5: Create MAP_SHARED mapping for populating hugetlbfs page cache */ shared_map = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, huge_fd, 0); if (shared_map == MAP_FAILED) { printf("[-] Failed to mmap MAP_SHARED: %s\n", strerror(errno)); exit(1); } printf("[+] mmap MAP_SHARED successful.\n"); for (size_t i = 0; i < NUM_PAGES; i++) { shared_map[i * HPAGE_SIZE] = 'A'; } printf("[+] Initial write to MAP_SHARED pages successful.\n"); /* Step 6: Create MAP_PRIVATE mappings and populate PTEs */ for (int k = 0; k < NUM_PRIV; k++) { priv_map[k] = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE, huge_fd, 0); if (priv_map[k] == MAP_FAILED) { printf("[-] Failed to mmap MAP_PRIVATE [%d]: %s\n", k, strerror(errno)); exit(1); } for (size_t i = 0; i < NUM_PAGES; i++) { volatile char c = priv_map[k][i * HPAGE_SIZE]; (void)c; } } printf("[+] Created %d MAP_PRIVATE mappings and faulted initial pages.\n", NUM_PRIV); /* Step 7: Pin main thread to CPU 0 and launch worker threads */ PIN_TO_CPU(0); pthread_t th[NUM_WORKERS]; for (long i = 0; i < NUM_WORKERS; i++) { res = pthread_create(&th[i], NULL, worker_routine, (void *)i); if (res != 0) { printf("[-] Failed to create worker thread %ld: %s\n", i, strerror(res)); exit(1); } } printf("[+] Launched %d worker contention threads successfully.\n", NUM_WORKERS); /* Step 8: Main truncation contention loop */ printf("[*] Starting main ftruncate race loop for 15 seconds...\n"); TIMER_START(t); unsigned long iterations = 0; while (TIMER_NOT_EXPIRED(t, 15.0)) { iterations++; /* Extend file to FILE_SIZE */ res = ftruncate(huge_fd, FILE_SIZE); if (res < 0) { printf("[-] Failed to ftruncate to FILE_SIZE at iteration %lu: %s\n", iterations, strerror(errno)); exit(1); } /* Write through shared_map to instantiate hugetlbfs folios in page cache */ for (size_t i = 0; i < NUM_PAGES; i++) { shared_map[i * HPAGE_SIZE] = (char)('A' + (iterations % 26)); } /* Fault pages in all private mappings so hugetlb_vma_maps_pfn() is satisfied */ for (int k = 0; k < NUM_PRIV; k++) { for (size_t i = 0; i < NUM_PAGES; i++) { volatile char c = priv_map[k][i * HPAGE_SIZE]; (void)c; } } /* Truncate to 0: races with workers holding resv_map->rw_sema */ res = ftruncate(huge_fd, 0); if (res < 0) { printf("[-] Failed to ftruncate to 0 at iteration %lu: %s\n", iterations, strerror(errno)); exit(1); } } printf("[+] Completed %lu contention loop iterations successfully.\n", iterations); /* Step 9: Stop workers and cleanup */ __atomic_store_n(&stop, 1, __ATOMIC_RELEASE); for (int i = 0; i < NUM_WORKERS; i++) { pthread_join(th[i], NULL); } printf("[+] All worker threads joined successfully.\n"); for (int k = 0; k < NUM_PRIV; k++) { res = munmap(priv_map[k], FILE_SIZE); if (res < 0) { printf("[-] Failed to munmap priv_map[%d]: %s\n", k, strerror(errno)); exit(1); } } printf("[+] munmap all private mappings successful.\n"); res = munmap(shared_map, FILE_SIZE); if (res < 0) { printf("[-] Failed to munmap shared mapping: %s\n", strerror(errno)); exit(1); } printf("[+] munmap shared mapping successful.\n"); res = close(huge_fd); if (res < 0) { printf("[-] Failed to close huge_fd: %s\n", strerror(errno)); exit(1); } printf("[+] close huge_fd successful.\n"); printf("[+] Reproducer finished.\n"); return 0; }