// https://syzkaller.appspot.com/bug?id=807e5f961536f1e0bcc7417248a4e0e4ba9a46f8 #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef EXT4_SUPER_MAGIC #define EXT4_SUPER_MAGIC 0xef53 #endif #ifndef FS_IOC_FSSETXATTR struct fsxattr { __u32 fsx_xflags; __u32 fsx_extsize; __u32 fsx_nextents; __u32 fsx_projid; __u32 fsx_cowextsize; unsigned char fsx_pad[8]; }; #define FS_IOC_FSSETXATTR _IOW('X', 32, struct fsxattr) #endif static char base_dir[512]; static void init_base_dir(void) { struct statfs st; char cwd[256]; if (getcwd(cwd, sizeof(cwd)) != NULL) { if (statfs(cwd, &st) == 0 && st.f_type == EXT4_SUPER_MAGIC) { snprintf(base_dir, sizeof(base_dir), "%s/ovl_repro", cwd); printf("[+] Detected ext4 filesystem at cwd: %s (f_type=0x%lx)\n", cwd, (unsigned long)st.f_type); return; } } if (statfs("/tmp", &st) == 0 && st.f_type == EXT4_SUPER_MAGIC) { snprintf(base_dir, sizeof(base_dir), "/tmp/ovl_repro"); printf("[+] Detected ext4 filesystem at /tmp (f_type=0x%lx)\n", (unsigned long)st.f_type); return; } if (statfs("/", &st) == 0 && st.f_type == EXT4_SUPER_MAGIC) { snprintf(base_dir, sizeof(base_dir), "/ovl_repro"); printf("[+] Detected ext4 filesystem at / (f_type=0x%lx)\n", (unsigned long)st.f_type); return; } if (getcwd(cwd, sizeof(cwd)) != NULL) { snprintf(base_dir, sizeof(base_dir), "%s/ovl_repro", cwd); } else { snprintf(base_dir, sizeof(base_dir), "/ovl_repro"); } statfs(base_dir, &st); printf("[!] Fallback base directory: %s (f_type=0x%lx)\n", base_dir, (unsigned long)st.f_type); } static void activate_ima_policy(void) { printf("[+] Activating IMA measurement policy...\n"); int res = mkdir("/sys/kernel/security", 0755); if (res < 0 && errno != EEXIST) { printf("[-] mkdir /sys/kernel/security failed: %s\n", strerror(errno)); } res = mount("securityfs", "/sys/kernel/security", "securityfs", 0, NULL); if (res < 0 && errno != EBUSY) { printf("[-] mount securityfs failed: %s\n", strerror(errno)); } else { printf("[+] securityfs mounted successfully.\n"); } int fd = open("/sys/kernel/security/ima/policy", O_WRONLY); if (fd < 0) { printf("[-] Failed to open /sys/kernel/security/ima/policy: %s\n", strerror(errno)); exit(1); } printf("[+] Opened /sys/kernel/security/ima/policy.\n"); const char rule[] = "measure func=FILE_CHECK\n"; ssize_t w = write(fd, rule, strlen(rule)); if (w < 0) { printf("[-] Failed to write rule to IMA policy: %s\n", strerror(errno)); close(fd); exit(1); } printf("[+] Wrote IMA policy rule successfully (%zd bytes).\n", w); res = close(fd); if (res < 0) { printf("[-] Failed to close IMA policy file: %s\n", strerror(errno)); exit(1); } printf("[+] IMA policy activated.\n"); } static void rm_rf(const char *path) { struct stat st; if (lstat(path, &st) < 0) return; if (S_ISDIR(st.st_mode)) { DIR *d = opendir(path); if (d) { struct dirent *de; char sub[1024]; while ((de = readdir(d)) != NULL) { if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; snprintf(sub, sizeof(sub), "%s/%s", path, de->d_name); rm_rf(sub); } closedir(d); } rmdir(path); } else { unlink(path); } } static void create_file(const char *path, const char *content) { int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd < 0) { printf("[-] Failed to create file %s: %s\n", path, strerror(errno)); exit(1); } ssize_t len = strlen(content); ssize_t w = write(fd, content, len); if (w < 0 || w != len) { printf("[-] Failed to write to file %s: %s\n", path, strerror(errno)); close(fd); exit(1); } int res = close(fd); if (res < 0) { printf("[-] Failed to close file %s: %s\n", path, strerror(errno)); exit(1); } printf("[+] Created file %s successfully.\n", path); } static void ioctl_copyup(const char *path) { int fd = open(path, O_RDONLY); if (fd < 0) { printf("[-] Failed to open %s for ioctl: %s\n", path, strerror(errno)); exit(1); } struct fsxattr fa = {0}; int res = ioctl(fd, FS_IOC_FSSETXATTR, &fa); if (res < 0) { printf("[-] ioctl(FS_IOC_FSSETXATTR) on %s failed: %s\n", path, strerror(errno)); close(fd); exit(1); } printf("[+] ioctl(FS_IOC_FSSETXATTR) on %s succeeded.\n", path); res = close(fd); if (res < 0) { printf("[-] Failed to close %s after ioctl: %s\n", path, strerror(errno)); exit(1); } } static void trunc_open(const char *path) { int fd = open(path, O_RDWR | O_TRUNC); if (fd < 0) { printf("[-] Failed to open %s with O_RDWR | O_TRUNC: %s\n", path, strerror(errno)); exit(1); } printf("[+] Opened %s with O_RDWR | O_TRUNC successfully.\n", path); int res = close(fd); if (res < 0) { printf("[-] Failed to close %s: %s\n", path, strerror(errno)); exit(1); } } int main(void) { char path[512]; char opts[1024]; setvbuf(stdout, NULL, _IONBF, 0); printf("[+] Starting reproducer for possible deadlock in ovl_copy_up_start...\n"); /* Step 0: Activate IMA policy */ activate_ima_policy(); /* Step 1: Detect base directory on ext4 */ init_base_dir(); /* Unmount previous mounts if present */ snprintf(path, sizeof(path), "%s/m2_merged", base_dir); umount2(path, MNT_DETACH); snprintf(path, sizeof(path), "%s/m1_merged", base_dir); umount2(path, MNT_DETACH); /* Clean up old files and directories */ rm_rf(base_dir); int res = mkdir(base_dir, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", base_dir, strerror(errno)); exit(1); } /* Create Mount 1 directories (depth = 0) */ snprintf(path, sizeof(path), "%s/m1_lower", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_upper", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_work", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_merged", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } /* Create Mount 2 directories (depth = 1) */ snprintf(path, sizeof(path), "%s/m2_upper", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m2_work", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m2_merged", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } printf("[+] Mount directories created successfully.\n"); /* Populate lower layer objects for both Mount 1 and Mount 2 */ snprintf(path, sizeof(path), "%s/m1_lower/file_copy1", base_dir); create_file(path, "test1"); snprintf(path, sizeof(path), "%s/m1_lower/file_copy2", base_dir); create_file(path, "test2"); snprintf(path, sizeof(path), "%s/m1_lower/dir_lookup1", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_lower/dir_lookup2", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_lower/dir_trigger1", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_lower/dir_trigger1_sub", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_lower/dir_trigger1_sub/sub", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_lower/dir_trigger2", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_lower/dir_trigger2_sub", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } snprintf(path, sizeof(path), "%s/m1_lower/dir_trigger2_sub/sub", base_dir); res = mkdir(path, 0777); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir %s: %s\n", path, strerror(errno)); exit(1); } printf("[+] Objects in lower layer prepared.\n"); /* Mount Base Overlayfs (Mount 1, s_stack_depth = 1, depth = 0) */ snprintf(opts, sizeof(opts), "lowerdir=%s/m1_lower,upperdir=%s/m1_upper,workdir=%s/m1_work,index=off", base_dir, base_dir, base_dir); snprintf(path, sizeof(path), "%s/m1_merged", base_dir); if (mount("overlay", path, "overlay", 0, opts) != 0) { printf("[-] Failed to mount Mount 1 overlay: %s\n", strerror(errno)); exit(1); } printf("[+] Mount 1 overlayfs mounted successfully at %s.\n", path); /* Mount Nested Overlayfs (Mount 2, s_stack_depth = 2, depth = 1) */ snprintf(opts, sizeof(opts), "lowerdir=%s/m1_merged,upperdir=%s/m2_upper,workdir=%s/m2_work,index=off", base_dir, base_dir, base_dir); snprintf(path, sizeof(path), "%s/m2_merged", base_dir); if (mount("overlay", path, "overlay", 0, opts) != 0) { printf("[-] Failed to mount Mount 2 nested overlay: %s\n", strerror(errno)); exit(1); } printf("[+] Mount 2 nested overlayfs mounted successfully at %s.\n", path); /* * Edge 1: ovl_i_lock_key -> sb_writers * Trigger copy up via FS_IOC_FSSETXATTR on uncopied files. * Calls ovl_copy_up_start() (holds ovl_i_lock_key) -> ovl_do_copy_up() -> * ovl_start_write() -> sb_start_write(upper_sb). */ printf("[+] Executing Edge 1: ovl_i_lock_key -> sb_writers...\n"); snprintf(path, sizeof(path), "%s/m1_merged/file_copy1", base_dir); ioctl_copyup(path); snprintf(path, sizeof(path), "%s/m2_merged/file_copy2", base_dir); ioctl_copyup(path); printf("[+] Edge 1 completed successfully.\n"); /* * Edge 2: sb_writers -> &ima_iint_mutex_key * Opening an existing regular file with O_RDWR | O_TRUNC acquires * mnt_want_write() (sb_writers) before calling security_file_post_open() -> * ima_file_check() -> process_measurement(), which acquires iint->mutex. */ printf("[+] Executing Edge 2: sb_writers -> ima_iint_mutex_key...\n"); snprintf(path, sizeof(path), "%s/ext4_trunc", base_dir); create_file(path, "x"); trunc_open(path); snprintf(path, sizeof(path), "%s/m1_merged/ovl_trunc1", base_dir); create_file(path, "y"); trunc_open(path); snprintf(path, sizeof(path), "%s/m2_merged/ovl_trunc2", base_dir); create_file(path, "z"); trunc_open(path); printf("[+] Edge 2 completed successfully.\n"); /* * Edge 3 & Edge 4: ima_iint_mutex_key -> of->mutex -> ovl_i_mutex_dir_key * Open /sys/power/resume with O_RDWR. * Opening with O_RDWR initializes of->mutex with the writable lockdep key. * Under IMA FILE_CHECK policy, security_file_post_open() calls process_measurement(), * which holds iint->mutex and reads from the file via kernfs_fop_read_iter(), * locking of->mutex (writable key). This establishes Edge 3. * Then writing an uncached path to the same fd invokes resume_store() under * of->mutex (writable key) -> lookup_bdev() -> lookup_slow(), acquiring * inode_lock_shared() on the overlayfs directory (ovl_i_mutex_dir_key). This establishes Edge 4. */ printf("[+] Executing Edge 3 & 4 via /sys/power/resume (O_RDWR)...\n"); int fd_resume = open("/sys/power/resume", O_RDWR); if (fd_resume < 0) { printf("[-] Failed to open /sys/power/resume with O_RDWR: %s\n", strerror(errno)); exit(1); } printf("[+] Opened /sys/power/resume with O_RDWR (Edge 3: ima_iint_mutex_key -> of->mutex).\n"); /* Drop caches to ensure lookup_slow is invoked */ int fd_drop = open("/proc/sys/vm/drop_caches", O_WRONLY); if (fd_drop >= 0) { ssize_t dw = write(fd_drop, "3\n", 2); if (dw < 0) { printf("[-] Warning: Failed to write to /proc/sys/vm/drop_caches: %s\n", strerror(errno)); } else { printf("[+] Dropped caches via /proc/sys/vm/drop_caches.\n"); } close(fd_drop); } else { printf("[-] Warning: Failed to open /proc/sys/vm/drop_caches: %s\n", strerror(errno)); } /* Edge 4: of->mutex -> ovl_i_mutex_dir_key[1] (Mount 2) */ char target[512]; snprintf(target, sizeof(target), "%s/m2_merged/dir_lookup2/nonexistent_bdev_1\n", base_dir); ssize_t w = write(fd_resume, target, strlen(target)); if (w < 0 && errno != EINVAL) { printf("[-] Failed to write target2 to /sys/power/resume: %s\n", strerror(errno)); close(fd_resume); exit(1); } printf("[+] Traversing Mount 2 directory in lookup_slow under of->mutex succeeded.\n"); /* Edge 4: of->mutex -> ovl_i_mutex_dir_key[0] (Mount 1) */ snprintf(target, sizeof(target), "%s/m1_merged/dir_lookup1/nonexistent_bdev_2\n", base_dir); w = write(fd_resume, target, strlen(target)); if (w < 0 && errno != EINVAL) { printf("[-] Failed to write target1 to /sys/power/resume: %s\n", strerror(errno)); close(fd_resume); exit(1); } printf("[+] Traversing Mount 1 directory in lookup_slow under of->mutex succeeded.\n"); res = close(fd_resume); if (res < 0) { printf("[-] Failed to close /sys/power/resume: %s\n", strerror(errno)); exit(1); } printf("[+] Edge 3 & 4 completed successfully.\n"); /* * Edge 0 (Deadlock Trigger): ovl_i_mutex_dir_key -> ovl_i_lock_key * Calling lsetxattr on an uncopied overlayfs directory takes inode_lock * (ovl_i_mutex_dir_key) in vfs_setxattr(), and then ovl_xattr_set() initiates * directory copy-up via ovl_copy_up_start(), acquiring ovl_inode_lock * (ovl_i_lock_key) on the same inode. * This closes the cycle and triggers the lockdep report. */ printf("[+] Executing Edge 0 (Trigger): ovl_i_mutex_dir_key -> ovl_i_lock_key...\n"); snprintf(path, sizeof(path), "%s/m2_merged/dir_trigger2", base_dir); res = lsetxattr(path, "user.test", "val", 3, 0); if (res < 0) { printf("[-] lsetxattr user.test on %s: %s\n", path, strerror(errno)); } else { printf("[+] lsetxattr user.test on %s succeeded.\n", path); } snprintf(path, sizeof(path), "%s/m2_merged/dir_trigger2_sub/sub", base_dir); res = lsetxattr(path, "user.test", "val", 3, 0); if (res < 0) { printf("[-] lsetxattr user.test on %s: %s\n", path, strerror(errno)); } else { printf("[+] lsetxattr user.test on %s succeeded.\n", path); } snprintf(path, sizeof(path), "%s/m1_merged/dir_trigger1", base_dir); res = lsetxattr(path, "user.test", "val", 3, 0); if (res < 0) { printf("[-] lsetxattr user.test on %s: %s\n", path, strerror(errno)); } else { printf("[+] lsetxattr user.test on %s succeeded.\n", path); } snprintf(path, sizeof(path), "%s/m1_merged/dir_trigger1_sub/sub", base_dir); res = lsetxattr(path, "user.test", "val", 3, 0); if (res < 0) { printf("[-] lsetxattr user.test on %s: %s\n", path, strerror(errno)); } else { printf("[+] lsetxattr user.test on %s succeeded.\n", path); } printf("[+] Trigger operations executed.\n"); /* Allow kernel logging to flush before cleaning up */ printf("[+] Sleeping 2 seconds to allow lockdep reports to flush...\n"); sleep(2); /* Cleanup mounts */ printf("[+] Unmounting filesystems...\n"); snprintf(path, sizeof(path), "%s/m2_merged", base_dir); umount2(path, MNT_DETACH); snprintf(path, sizeof(path), "%s/m1_merged", base_dir); umount2(path, MNT_DETACH); printf("[+] Cleanup complete. Reproducer finished.\n"); return 0; }