// https://syzkaller.appspot.com/bug?id=9c0b61a007c7ff8c0167a150d86a26563f3e1406 #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 #include #include #include #include #define MINIX_BLOCK_SIZE 1024 #define IMAGE_SIZE (256 * MINIX_BLOCK_SIZE) struct minix_v1_dir_entry { uint16_t inode; char name[14]; }; static volatile int stop_flag = 0; static int num_online_cpus = 1; static int eio_reported = 0; static void *sync_worker_1(void *arg) { int mnt_fd = *(int *)arg; PIN_TO_CPU(0); while (!stop_flag) { syncfs(mnt_fd); sync(); usleep(50); } return NULL; } static void *sync_worker_2(void *arg) { int mnt_fd = *(int *)arg; PIN_TO_CPU(0); while (!stop_flag) { syncfs(mnt_fd); usleep(20); } return NULL; } static void *mkdir_worker(void *arg) { int worker_id = *(int *)arg; PIN_TO_CPU(1 % num_online_cpus); char dirpath[128]; int count = 0; while (!stop_flag) { snprintf(dirpath, sizeof(dirpath), "/tmp/minix_mnt/w%d_%d", worker_id, count % 16); int res = mkdir(dirpath, 0700); if (res < 0) { if (errno == EIO && !eio_reported) { printf("[+] mkdir encountered expected EIO during directory sync failure: %s\n", strerror(errno)); eio_reported = 1; } } else { rmdir(dirpath); } count++; usleep(100); } return NULL; } int main(void) { setvbuf(stdout, NULL, _IONBF, 0); printf("[*] Starting Minix V1 truncate buffer uptodate warning reproducer...\n"); num_online_cpus = sysconf(_SC_NPROCESSORS_ONLN); if (num_online_cpus < 1) num_online_cpus = 1; printf("[+] Detected %d online CPU(s).\n", num_online_cpus); /* 1. Build the 256KB Minix V1 filesystem image in memory */ uint8_t *image = calloc(1, IMAGE_SIZE); if (!image) { printf("[-] Failed to allocate image buffer: %s\n", strerror(errno)); exit(1); } /* Superblock at Block 1 (offset 1024) */ struct minix_super_block *sb = (struct minix_super_block *)(image + 1024); sb->s_ninodes = 32; sb->s_nzones = 256; sb->s_imap_blocks = 1; sb->s_zmap_blocks = 1; sb->s_firstdatazone = 8; sb->s_log_zone_size = 0; sb->s_max_size = 268966912; /* Exact max size: (7 + 512 + 512 * 512) * 1024 */ sb->s_magic = 0x137F; /* MINIX_SUPER_MAGIC */ sb->s_state = 1; /* MINIX_VALID_FS */ sb->s_zones = 0; /* Inode Bitmap at Block 2 (offset 2048): byte 0 = 0xFF marks inodes 0-7 as used, next free is inode 8 */ image[2048] = 0xFF; /* Zone Bitmap at Block 3 (offset 3072): bit 0 (reserved) and bit 1 (root dir block 8) set -> 0x03 */ image[3072] = 0x03; /* Inode Table at Block 4 (offset 4096): Inode 1 is root directory */ struct minix_inode *root_ino = (struct minix_inode *)(image + 4096); root_ino->i_mode = S_IFDIR | 0755; root_ino->i_uid = 0; root_ino->i_gid = 0; root_ino->i_nlinks = 2; root_ino->i_size = 32; root_ino->i_time = 0; root_ino->i_zone[0] = 8; /* Data Block 8 (offset 8192): Root directory entries "." and ".." */ struct minix_v1_dir_entry *de = (struct minix_v1_dir_entry *)(image + 8192); de[0].inode = 1; strncpy(de[0].name, ".", sizeof(de[0].name)); de[1].inode = 1; strncpy(de[1].name, "..", sizeof(de[1].name)); /* 2. Write the filesystem image to /tmp/minix.img */ const char *img_path = "/tmp/minix.img"; int img_fd = open(img_path, O_RDWR | O_CREAT | O_TRUNC, 0666); if (img_fd < 0) { printf("[-] Failed to open %s: %s\n", img_path, strerror(errno)); exit(1); } printf("[+] Opened %s successfully.\n", img_path); ssize_t written = write(img_fd, image, IMAGE_SIZE); if (written != (ssize_t)IMAGE_SIZE) { printf("[-] Failed to write complete image to %s: %s\n", img_path, strerror(errno)); exit(1); } printf("[+] Wrote 256KB Minix V1 image data successfully.\n"); int res = fsync(img_fd); if (res < 0) { printf("[-] Failed to fsync %s: %s\n", img_path, strerror(errno)); exit(1); } printf("[+] fsync %s successful.\n", img_path); free(image); /* 3. Obtain a free loop device via /dev/loop-control */ int ctl_fd = open("/dev/loop-control", O_RDWR); if (ctl_fd < 0) { printf("[-] Failed to open /dev/loop-control: %s\n", strerror(errno)); exit(1); } printf("[+] Opened /dev/loop-control successfully.\n"); int loop_idx = ioctl(ctl_fd, LOOP_CTL_GET_FREE); if (loop_idx < 0) { printf("[-] Failed to get free loop device from /dev/loop-control: %s\n", strerror(errno)); exit(1); } printf("[+] Obtained free loop device index: %d\n", loop_idx); res = close(ctl_fd); if (res < 0) { printf("[-] Failed to close /dev/loop-control: %s\n", strerror(errno)); exit(1); } if (loop_idx > 31) { printf("[-] Loop device index %d exceeds maximum allowed index of 31\n", loop_idx); exit(1); } char loop_path[32]; snprintf(loop_path, sizeof(loop_path), "/dev/loop%d", loop_idx); int loop_fd = open(loop_path, O_RDWR); if (loop_fd < 0) { printf("[-] Failed to open %s: %s\n", loop_path, strerror(errno)); exit(1); } printf("[+] Opened %s successfully.\n", loop_path); /* 4. Associate the backing file with the loop device */ ioctl(loop_fd, LOOP_CLR_FD, 0); res = ioctl(loop_fd, LOOP_SET_FD, img_fd); if (res < 0) { printf("[-] Failed to set backing file on %s via LOOP_SET_FD: %s\n", loop_path, strerror(errno)); exit(1); } printf("[+] Successfully associated %s with backing file %s.\n", loop_path, img_path); /* Close loop_fd immediately to avoid EBUSY during mount() */ res = close(loop_fd); if (res < 0) { printf("[-] Failed to close loop_fd before mount: %s\n", strerror(errno)); exit(1); } printf("[+] Closed %s before mount to ensure mutual exclusion compliance.\n", loop_path); /* 5. Create mount point directory and mount filesystem */ const char *mnt_path = "/tmp/minix_mnt"; res = mkdir(mnt_path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to create mount point %s: %s\n", mnt_path, strerror(errno)); exit(1); } printf("[+] Mount directory %s ready.\n", mnt_path); res = mount(loop_path, mnt_path, "minix", 0, NULL); if (res < 0) { printf("[-] Failed to mount %s on %s as minix: %s\n", loop_path, mnt_path, strerror(errno)); exit(1); } printf("[+] Successfully mounted %s on %s as minix.\n", loop_path, mnt_path); /* Open mount directory to access and cache root directory */ int mnt_fd = open(mnt_path, O_RDONLY | O_DIRECTORY); if (mnt_fd < 0) { printf("[-] Failed to open mount directory %s: %s\n", mnt_path, strerror(errno)); exit(1); } printf("[+] Opened mount directory %s successfully.\n", mnt_path); /* 6. Post-mount device capacity reduction: open loop device O_RDONLY to avoid EBUSY */ int loop_ro_fd = open(loop_path, O_RDONLY); if (loop_ro_fd < 0) { printf("[-] Failed to open %s O_RDONLY: %s\n", loop_path, strerror(errno)); exit(1); } printf("[+] Opened %s O_RDONLY successfully.\n", loop_path); struct loop_info64 loop_info; memset(&loop_info, 0, sizeof(loop_info)); res = ioctl(loop_ro_fd, LOOP_GET_STATUS64, &loop_info); if (res < 0) { printf("[-] Failed to get loop info via LOOP_GET_STATUS64: %s\n", strerror(errno)); exit(1); } printf("[+] Retrieved loop status successfully.\n"); loop_info.lo_sizelimit = 2048; res = ioctl(loop_ro_fd, LOOP_SET_STATUS64, &loop_info); if (res < 0) { printf("[-] Failed to set lo_sizelimit=2048 via LOOP_SET_STATUS64: %s\n", strerror(errno)); exit(1); } printf("[+] Successfully updated loop device size limit to 2048 bytes.\n"); res = close(loop_ro_fd); if (res < 0) { printf("[-] Failed to close loop_ro_fd: %s\n", strerror(errno)); exit(1); } printf("[+] Closed loop_ro_fd successfully.\n"); /* Truncate backing file descriptor to 2048 bytes to guarantee writeback I/O errors */ res = ftruncate(img_fd, 2048); if (res < 0) { printf("[-] Failed to ftruncate backing file: %s\n", strerror(errno)); exit(1); } printf("[+] Successfully truncated backing file to 2048 bytes.\n"); /* 7. Start concurrent sync threads and worker threads to trigger the race */ pthread_t th_sync1, th_sync2, th_mkdir; int worker_id = 1; res = pthread_create(&th_sync1, NULL, sync_worker_1, &mnt_fd); if (res != 0) { printf("[-] Failed to create sync worker 1: %s\n", strerror(res)); exit(1); } printf("[+] Created sync worker thread 1.\n"); res = pthread_create(&th_sync2, NULL, sync_worker_2, &mnt_fd); if (res != 0) { printf("[-] Failed to create sync worker 2: %s\n", strerror(res)); exit(1); } printf("[+] Created sync worker thread 2.\n"); res = pthread_create(&th_mkdir, NULL, mkdir_worker, &worker_id); if (res != 0) { printf("[-] Failed to create mkdir worker: %s\n", strerror(res)); exit(1); } printf("[+] Created mkdir worker thread.\n"); /* 8. Main thread runs mkdir/rmdir operations concurrently for 6 seconds */ printf("[*] Executing race condition loop for 6 seconds...\n"); TIMER_START(race_timer); int main_count = 0; char dirpath[128]; while (TIMER_NOT_EXPIRED(race_timer, 6.0)) { snprintf(dirpath, sizeof(dirpath), "/tmp/minix_mnt/main_d_%d", main_count % 16); int m_res = mkdir(dirpath, 0700); if (m_res < 0) { if (errno == EIO && !eio_reported) { printf("[+] mkdir encountered expected EIO during directory sync failure: %s\n", strerror(errno)); eio_reported = 1; } } else { rmdir(dirpath); } main_count++; usleep(150); } stop_flag = 1; printf("[+] Race condition loop completed after %d iterations.\n", main_count); res = pthread_join(th_sync1, NULL); if (res != 0) printf("[-] Failed to join sync worker 1: %s\n", strerror(res)); res = pthread_join(th_sync2, NULL); if (res != 0) printf("[-] Failed to join sync worker 2: %s\n", strerror(res)); res = pthread_join(th_mkdir, NULL); if (res != 0) printf("[-] Failed to join mkdir worker: %s\n", strerror(res)); res = close(mnt_fd); if (res < 0) printf("[-] Failed to close mount directory fd: %s\n", strerror(errno)); res = close(img_fd); if (res < 0) printf("[-] Failed to close backing file fd: %s\n", strerror(errno)); /* Sleep to allow asynchronous kernel logging and completion */ sleep(2); printf("[+] Reproducer finished.\n"); return 0; }