// https://syzkaller.appspot.com/bug?id=a3c2b85a9658da5e97d4a9ba07a395886f9eed3d #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include static int get_net_prio_cgroup_opts(char *opts_buf, size_t max_len) { FILE *fp = fopen("/proc/self/cgroup", "r"); if (!fp) { printf("[-] Failed to open /proc/self/cgroup: %s\n", strerror(errno)); return -1; } char line[1024]; int found = 0; while (fgets(line, sizeof(line), fp)) { /* Format: hierarchy-ID:controller-list:path */ if (strstr(line, "net_prio")) { char *first_colon = strchr(line, ':'); if (first_colon) { char *second_colon = strchr(first_colon + 1, ':'); if (second_colon) { size_t len = (size_t)(second_colon - (first_colon + 1)); if (len > 0 && len < max_len) { strncpy(opts_buf, first_colon + 1, len); opts_buf[len] = '\0'; found = 1; break; } } } } } fclose(fp); if (!found) { strncpy(opts_buf, "blkio,devices,freezer,net_prio", max_len - 1); opts_buf[max_len - 1] = '\0'; } return 0; } static void cleanup_previous_mount(void) { FILE *fp = fopen("/proc/mounts", "r"); if (fp) { char line[1024]; int mounted = 0; while (fgets(line, sizeof(line), fp)) { if (strstr(line, " /tmp/cg ")) { mounted = 1; break; } } fclose(fp); if (mounted) { int res = umount2("/tmp/cg", MNT_DETACH); if (res < 0) { printf("[-] Failed to unmount existing /tmp/cg: %s\n", strerror(errno)); } else { printf("[+] Unmounted previous /tmp/cg mount.\n"); } } } } int main(void) { setvbuf(stdout, NULL, _IONBF, 0); printf("[*] Starting reproducer for circular locking in cgroup_kn_lock_live...\n"); /* Ensure mount target is clean */ cleanup_previous_mount(); /* Step 1: Determine cgroup v1 controllers co-mounted with net_prio */ char cgroup_opts[256]; int res = get_net_prio_cgroup_opts(cgroup_opts, sizeof(cgroup_opts)); if (res < 0) { printf("[-] Failed to detect cgroup mount options\n"); exit(1); } printf("[+] Detected cgroup mount options: %s\n", cgroup_opts); /* Step 2: Create mount directory and mount cgroup hierarchy */ res = mkdir("/tmp/cg", 0755); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir /tmp/cg: %s\n", strerror(errno)); exit(1); } printf("[+] mkdir /tmp/cg successful.\n"); res = mount("none", "/tmp/cg", "cgroup", 0, cgroup_opts); if (res < 0) { printf("[-] Failed to mount cgroup with detected options: %s, trying fallback\n", strerror(errno)); res = mount("none", "/tmp/cg", "cgroup", 0, "blkio,devices,freezer,net_prio"); if (res < 0) { printf("[-] Failed to mount cgroup with fallback options: %s\n", strerror(errno)); rmdir("/tmp/cg"); exit(1); } } printf("[+] mount cgroup v1 successful.\n"); /* Step 3: Create child cgroup directory */ /* This executes cgroup_mkdir -> cgrp_css_online -> rtnl_lock(), establishing the dependency: cgroup_mutex -> rtnl_mutex */ res = mkdir("/tmp/cg/sub", 0755); if (res < 0 && errno != EEXIST) { printf("[-] Failed to mkdir /tmp/cg/sub: %s\n", strerror(errno)); umount2("/tmp/cg", MNT_DETACH); rmdir("/tmp/cg"); exit(1); } printf("[+] mkdir /tmp/cg/sub successful (cgroup_mutex -> rtnl_mutex established).\n"); /* Step 4: Write device paths to /sys/module/block2mtd/parameters/block2mtd */ /* Under param_lock, block2mtd_setup performs path lookup on the block device path. Resolving an uncached path under kernfs/cgroup invokes lookup_slow, which takes inode_lock_shared (&type->i_mutex_dir_key#6). This establishes the dependency: param_lock -> &type->i_mutex_dir_key#6 */ int param_fd = open("/sys/module/block2mtd/parameters/block2mtd", O_WRONLY); if (param_fd < 0) { printf("[-] Failed to open /sys/module/block2mtd/parameters/block2mtd: %s\n", strerror(errno)); rmdir("/tmp/cg/sub"); umount2("/tmp/cg", MNT_DETACH); rmdir("/tmp/cg"); exit(1); } printf("[+] open /sys/module/block2mtd/parameters/block2mtd successful.\n"); const char p1[] = "/tmp/cg/nonexistent_mtd_bdev\n"; ssize_t nwritten = write(param_fd, p1, strlen(p1)); if (nwritten < 0) { printf("[-] Failed to write path '%s' to block2mtd: %s\n", p1, strerror(errno)); close(param_fd); rmdir("/tmp/cg/sub"); umount2("/tmp/cg", MNT_DETACH); rmdir("/tmp/cg"); exit(1); } printf("[+] write path '%s' to block2mtd successful.\n", p1); const char p2[] = "/dev/nonexistent_mtd_bdev\n"; nwritten = write(param_fd, p2, strlen(p2)); if (nwritten < 0) { printf("[-] Failed to write path '%s' to block2mtd: %s\n", p2, strerror(errno)); close(param_fd); rmdir("/tmp/cg/sub"); umount2("/tmp/cg", MNT_DETACH); rmdir("/tmp/cg"); exit(1); } printf("[+] write path '%s' to block2mtd successful (param_lock -> i_mutex_dir_key established).\n", p2); res = close(param_fd); if (res < 0) { printf("[-] Failed to close /sys/module/block2mtd/parameters/block2mtd: %s\n", strerror(errno)); exit(1); } printf("[+] close block2mtd param_fd successful.\n"); /* Step 5: Remove child cgroup directory */ /* vfs_rmdir holds inode_lock (&type->i_mutex_dir_key#6) and invokes cgroup_rmdir -> cgroup_kn_lock_live, which acquires cgroup_mutex. This closes the cycle: cgroup_mutex -> rtnl_mutex -> param_lock -> i_mutex_dir_key -> cgroup_mutex triggering the circular locking dependency warning in lockdep. */ res = rmdir("/tmp/cg/sub"); if (res < 0) { printf("[-] Failed to rmdir /tmp/cg/sub: %s\n", strerror(errno)); umount2("/tmp/cg", MNT_DETACH); rmdir("/tmp/cg"); exit(1); } printf("[+] rmdir /tmp/cg/sub successful (lockdep cycle triggered!).\n"); sleep(1); /* Step 6: Cleanup */ res = umount2("/tmp/cg", MNT_DETACH); if (res < 0) { printf("[-] Failed to umount2 /tmp/cg: %s\n", strerror(errno)); exit(1); } printf("[+] umount2 /tmp/cg successful.\n"); res = rmdir("/tmp/cg"); if (res < 0) { printf("[-] Failed to rmdir /tmp/cg: %s\n", strerror(errno)); exit(1); } printf("[+] rmdir /tmp/cg successful.\n"); printf("[+] Reproducer finished successfully.\n"); return 0; }