| 1 | |
| 2 | |
| 3 | * This is for all the tests related to copy_to_user() and copy_from_user()
|
| 4 | * hardening.
|
| 5 |
|
| 6 | #include "lkdtm.h"
|
| 7 | #include <linux/slab.h>
|
| 8 | #include <linux/highmem.h>
|
| 9 | #include <linux/vmalloc.h>
|
| 10 | #include <linux/sched/task_stack.h>
|
| 11 | #include <linux/mman.h>
|
| 12 | #include <linux/uaccess.h>
|
| 13 | #include <asm/cacheflush.h>
|
| 14 |
|
| 15 | |
| 16 | * Many of the tests here end up using const sizes, but those would
|
| 17 | * normally be ignored by hardened usercopy, so force the compiler
|
| 18 | * into choosing the non-const path to make sure we trigger the
|
| 19 | * hardened usercopy checks by added "unconst" to all the const copies,
|
| 20 | * and making sure "cache_size" isn't optimized into a const.
|
| 21 |
|
| 22 | static volatile size_t unconst;
|
| 23 | static volatile size_t cache_size = 1024;
|
| 24 | static struct kmem_cache *whitelist_cache;
|
| 25 |
|
| 26 | static const unsigned char test_text[] = "This is a test.\n";
|
| 27 |
|
| 28 | |
| 29 | * Instead of adding -Wno-return-local-addr, just pass the stack address
|
| 30 | * through a function to obfuscate it from the compiler.
|
| 31 |
|
| 32 | static noinline unsigned char *trick_compiler(unsigned char *stack)
|
| 33 | {
|
| 34 | return stack + unconst;
|
| 35 | }
|
| 36 |
|
| 37 | static noinline unsigned char *do_usercopy_stack_callee(int value)
|
| 38 | {
|
| 39 | unsigned char buf[128];
|
| 40 | int i;
|
| 41 |
|
| 42 |
|
| 43 | for (i = 0; i < sizeof(buf); i++) {
|
| 44 | buf[i] = value & 0xff;
|
| 45 | }
|
| 46 |
|
| 47 | |
| 48 | * Put the target buffer in the middle of stack allocation
|
| 49 | * so that we don't step on future stack users regardless
|
| 50 | * of stack growth direction.
|
| 51 |
|
| 52 | return trick_compiler(&buf[(128/2)-32]);
|
| 53 | }
|
| 54 |
|
| 55 | static noinline void do_usercopy_stack(bool to_user, bool bad_frame)
|
| 56 | {
|
| 57 | unsigned long user_addr;
|
| 58 | unsigned char good_stack[32];
|
| 59 | unsigned char *bad_stack;
|
| 60 | int i;
|
| 61 |
|
| 62 |
|
| 63 | for (i = 0; i < sizeof(good_stack); i++)
|
| 64 | good_stack[i] = test_text[i % sizeof(test_text)];
|
| 65 |
|
| 66 |
|
| 67 | if (bad_frame) {
|
| 68 | bad_stack = do_usercopy_stack_callee((uintptr_t)&bad_stack);
|
| 69 | } else {
|
| 70 |
|
| 71 | bad_stack = task_stack_page(current) + THREAD_SIZE;
|
| 72 | bad_stack -= sizeof(unsigned long);
|
| 73 | }
|
| 74 |
|
| 75 | #ifdef ARCH_HAS_CURRENT_STACK_POINTER
|
| 76 | pr_info("stack : %px\n", (void *)current_stack_pointer);
|
| 77 | #endif
|
| 78 | pr_info("good_stack: %px-%px\n", good_stack, good_stack + sizeof(good_stack));
|
| 79 | pr_info("bad_stack : %px-%px\n", bad_stack, bad_stack + sizeof(good_stack));
|
| 80 |
|
| 81 | user_addr = vm_mmap(NULL, 0, PAGE_SIZE,
|
| 82 | PROT_READ | PROT_WRITE | PROT_EXEC,
|
| 83 | MAP_ANONYMOUS | MAP_PRIVATE, 0);
|
| 84 | if (user_addr >= TASK_SIZE) {
|
| 85 | pr_warn("Failed to allocate user memory\n");
|
| 86 | return;
|
| 87 | }
|
| 88 |
|
| 89 | if (to_user) {
|
| 90 | pr_info("attempting good copy_to_user of local stack\n");
|
| 91 | if (copy_to_user((void __user *)user_addr, good_stack,
|
| 92 | unconst + sizeof(good_stack))) {
|
| 93 | pr_warn("copy_to_user failed unexpectedly?!\n");
|
| 94 | goto free_user;
|
| 95 | }
|
| 96 |
|
| 97 | pr_info("attempting bad copy_to_user of distant stack\n");
|
| 98 | if (copy_to_user((void __user *)user_addr, bad_stack,
|
| 99 | unconst + sizeof(good_stack))) {
|
| 100 | pr_warn("copy_to_user failed, but lacked Oops\n");
|
| 101 | goto free_user;
|
| 102 | }
|
| 103 | } else {
|
| 104 | |
| 105 | * There isn't a safe way to not be protected by usercopy
|
| 106 | * if we're going to write to another thread's stack.
|
| 107 |
|
| 108 | if (!bad_frame)
|
| 109 | goto free_user;
|
| 110 |
|
| 111 | pr_info("attempting good copy_from_user of local stack\n");
|
| 112 | if (copy_from_user(good_stack, (void __user *)user_addr,
|
| 113 | unconst + sizeof(good_stack))) {
|
| 114 | pr_warn("copy_from_user failed unexpectedly?!\n");
|
| 115 | goto free_user;
|
| 116 | }
|
| 117 |
|
| 118 | pr_info("attempting bad copy_from_user of distant stack\n");
|
| 119 | if (copy_from_user(bad_stack, (void __user *)user_addr,
|
| 120 | unconst + sizeof(good_stack))) {
|
| 121 | pr_warn("copy_from_user failed, but lacked Oops\n");
|
| 122 | goto free_user;
|
| 123 | }
|
| 124 | }
|
| 125 |
|
| 126 | free_user:
|
| 127 | vm_munmap(user_addr, PAGE_SIZE);
|
| 128 | }
|
| 129 |
|
| 130 | |
| 131 | * This checks for whole-object size validation with hardened usercopy,
|
| 132 | * with or without usercopy whitelisting.
|
| 133 |
|
| 134 | static void do_usercopy_slab_size(bool to_user)
|
| 135 | {
|
| 136 | unsigned long user_addr;
|
| 137 | unsigned char *one, *two;
|
| 138 | void __user *test_user_addr;
|
| 139 | void *test_kern_addr;
|
| 140 | size_t size = unconst + 1024;
|
| 141 |
|
| 142 | one = kmalloc(size, GFP_KERNEL);
|
| 143 | two = kmalloc(size, GFP_KERNEL);
|
| 144 | if (!one || !two) {
|
| 145 | pr_warn("Failed to allocate kernel memory\n");
|
| 146 | goto free_kernel;
|
| 147 | }
|
| 148 |
|
| 149 | user_addr = vm_mmap(NULL, 0, PAGE_SIZE,
|
| 150 | PROT_READ | PROT_WRITE | PROT_EXEC,
|
| 151 | MAP_ANONYMOUS | MAP_PRIVATE, 0);
|
| 152 | if (user_addr >= TASK_SIZE) {
|
| 153 | pr_warn("Failed to allocate user memory\n");
|
| 154 | goto free_kernel;
|
| 155 | }
|
| 156 |
|
| 157 | memset(one, 'A', size);
|
| 158 | memset(two, 'B', size);
|
| 159 |
|
| 160 | test_user_addr = (void __user *)(user_addr + 16);
|
| 161 | test_kern_addr = one + 16;
|
| 162 |
|
| 163 | if (to_user) {
|
| 164 | pr_info("attempting good copy_to_user of correct size\n");
|
| 165 | if (copy_to_user(test_user_addr, test_kern_addr, size / 2)) {
|
| 166 | pr_warn("copy_to_user failed unexpectedly?!\n");
|
| 167 | goto free_user;
|
| 168 | }
|
| 169 |
|
| 170 | pr_info("attempting bad copy_to_user of too large size\n");
|
| 171 | if (copy_to_user(test_user_addr, test_kern_addr, size)) {
|
| 172 | pr_warn("copy_to_user failed, but lacked Oops\n");
|
| 173 | goto free_user;
|
| 174 | }
|
| 175 | } else {
|
| 176 | pr_info("attempting good copy_from_user of correct size\n");
|
| 177 | if (copy_from_user(test_kern_addr, test_user_addr, size / 2)) {
|
| 178 | pr_warn("copy_from_user failed unexpectedly?!\n");
|
| 179 | goto free_user;
|
| 180 | }
|
| 181 |
|
| 182 | pr_info("attempting bad copy_from_user of too large size\n");
|
| 183 | if (copy_from_user(test_kern_addr, test_user_addr, size)) {
|
| 184 | pr_warn("copy_from_user failed, but lacked Oops\n");
|
| 185 | goto free_user;
|
| 186 | }
|
| 187 | }
|
| 188 | pr_err("FAIL: bad usercopy not detected!\n");
|
| 189 | pr_expected_config_param(CONFIG_HARDENED_USERCOPY, "hardened_usercopy");
|
| 190 |
|
| 191 | free_user:
|
| 192 | vm_munmap(user_addr, PAGE_SIZE);
|
| 193 | free_kernel:
|
| 194 | kfree(one);
|
| 195 | kfree(two);
|
| 196 | }
|
| 197 |
|
| 198 | |
| 199 | * This checks for the specific whitelist window within an object. If this
|
| 200 | * test passes, then do_usercopy_slab_size() tests will pass too.
|
| 201 |
|
| 202 | static void do_usercopy_slab_whitelist(bool to_user)
|
| 203 | {
|
| 204 | unsigned long user_alloc;
|
| 205 | unsigned char *buf = NULL;
|
| 206 | unsigned char __user *user_addr;
|
| 207 | size_t offset, size;
|
| 208 |
|
| 209 |
|
| 210 | if (!whitelist_cache) {
|
| 211 | pr_warn("Failed to allocate kernel cache\n");
|
| 212 | return;
|
| 213 | }
|
| 214 |
|
| 215 | |
| 216 | * Allocate a buffer with a whitelisted window in the buffer.
|
| 217 |
|
| 218 | buf = kmem_cache_alloc(whitelist_cache, GFP_KERNEL);
|
| 219 | if (!buf) {
|
| 220 | pr_warn("Failed to allocate buffer from whitelist cache\n");
|
| 221 | goto free_alloc;
|
| 222 | }
|
| 223 |
|
| 224 |
|
| 225 | user_alloc = vm_mmap(NULL, 0, PAGE_SIZE,
|
| 226 | PROT_READ | PROT_WRITE | PROT_EXEC,
|
| 227 | MAP_ANONYMOUS | MAP_PRIVATE, 0);
|
| 228 | if (user_alloc >= TASK_SIZE) {
|
| 229 | pr_warn("Failed to allocate user memory\n");
|
| 230 | goto free_alloc;
|
| 231 | }
|
| 232 | user_addr = (void __user *)user_alloc;
|
| 233 |
|
| 234 | memset(buf, 'B', cache_size);
|
| 235 |
|
| 236 |
|
| 237 | offset = (cache_size / 4) + unconst;
|
| 238 | size = (cache_size / 16) + unconst;
|
| 239 |
|
| 240 | if (to_user) {
|
| 241 | pr_info("attempting good copy_to_user inside whitelist\n");
|
| 242 | if (copy_to_user(user_addr, buf + offset, size)) {
|
| 243 | pr_warn("copy_to_user failed unexpectedly?!\n");
|
| 244 | goto free_user;
|
| 245 | }
|
| 246 |
|
| 247 | pr_info("attempting bad copy_to_user outside whitelist\n");
|
| 248 | if (copy_to_user(user_addr, buf + offset - 1, size)) {
|
| 249 | pr_warn("copy_to_user failed, but lacked Oops\n");
|
| 250 | goto free_user;
|
| 251 | }
|
| 252 | } else {
|
| 253 | pr_info("attempting good copy_from_user inside whitelist\n");
|
| 254 | if (copy_from_user(buf + offset, user_addr, size)) {
|
| 255 | pr_warn("copy_from_user failed unexpectedly?!\n");
|
| 256 | goto free_user;
|
| 257 | }
|
| 258 |
|
| 259 | pr_info("attempting bad copy_from_user outside whitelist\n");
|
| 260 | if (copy_from_user(buf + offset - 1, user_addr, size)) {
|
| 261 | pr_warn("copy_from_user failed, but lacked Oops\n");
|
| 262 | goto free_user;
|
| 263 | }
|
| 264 | }
|
| 265 | pr_err("FAIL: bad usercopy not detected!\n");
|
| 266 | pr_expected_config_param(CONFIG_HARDENED_USERCOPY, "hardened_usercopy");
|
| 267 |
|
| 268 | free_user:
|
| 269 | vm_munmap(user_alloc, PAGE_SIZE);
|
| 270 | free_alloc:
|
| 271 | if (buf)
|
| 272 | kmem_cache_free(whitelist_cache, buf);
|
| 273 | }
|
| 274 |
|
| 275 |
|
| 276 | static void lkdtm_USERCOPY_SLAB_SIZE_TO(void)
|
| 277 | {
|
| 278 | do_usercopy_slab_size(true);
|
| 279 | }
|
| 280 |
|
| 281 | static void lkdtm_USERCOPY_SLAB_SIZE_FROM(void)
|
| 282 | {
|
| 283 | do_usercopy_slab_size(false);
|
| 284 | }
|
| 285 |
|
| 286 | static void lkdtm_USERCOPY_SLAB_WHITELIST_TO(void)
|
| 287 | {
|
| 288 | do_usercopy_slab_whitelist(true);
|
| 289 | }
|
| 290 |
|
| 291 | static void lkdtm_USERCOPY_SLAB_WHITELIST_FROM(void)
|
| 292 | {
|
| 293 | do_usercopy_slab_whitelist(false);
|
| 294 | }
|
| 295 |
|
| 296 | static void lkdtm_USERCOPY_STACK_FRAME_TO(void)
|
| 297 | {
|
| 298 | do_usercopy_stack(true, true);
|
| 299 | }
|
| 300 |
|
| 301 | static void lkdtm_USERCOPY_STACK_FRAME_FROM(void)
|
| 302 | {
|
| 303 | do_usercopy_stack(false, true);
|
| 304 | }
|
| 305 |
|
| 306 | static void lkdtm_USERCOPY_STACK_BEYOND(void)
|
| 307 | {
|
| 308 | do_usercopy_stack(true, false);
|
| 309 | }
|
| 310 |
|
| 311 | static void lkdtm_USERCOPY_KERNEL(void)
|
| 312 | {
|
| 313 | unsigned long user_addr;
|
| 314 |
|
| 315 | user_addr = vm_mmap(NULL, 0, PAGE_SIZE,
|
| 316 | PROT_READ | PROT_WRITE | PROT_EXEC,
|
| 317 | MAP_ANONYMOUS | MAP_PRIVATE, 0);
|
| 318 | if (user_addr >= TASK_SIZE) {
|
| 319 | pr_warn("Failed to allocate user memory\n");
|
| 320 | return;
|
| 321 | }
|
| 322 |
|
| 323 | pr_info("attempting good copy_to_user from kernel rodata: %px\n",
|
| 324 | test_text);
|
| 325 | if (copy_to_user((void __user *)user_addr, test_text,
|
| 326 | unconst + sizeof(test_text))) {
|
| 327 | pr_warn("copy_to_user failed unexpectedly?!\n");
|
| 328 | goto free_user;
|
| 329 | }
|
| 330 |
|
| 331 | pr_info("attempting bad copy_to_user from kernel text: %px\n",
|
| 332 | vm_mmap);
|
| 333 | if (copy_to_user((void __user *)user_addr, vm_mmap,
|
| 334 | unconst + PAGE_SIZE)) {
|
| 335 | pr_warn("copy_to_user failed, but lacked Oops\n");
|
| 336 | goto free_user;
|
| 337 | }
|
| 338 | pr_err("FAIL: bad copy_to_user() not detected!\n");
|
| 339 | pr_expected_config_param(CONFIG_HARDENED_USERCOPY, "hardened_usercopy");
|
| 340 |
|
| 341 | free_user:
|
| 342 | vm_munmap(user_addr, PAGE_SIZE);
|
| 343 | }
|
| 344 |
|
| 345 | |
| 346 | * This expects "kaddr" to point to a PAGE_SIZE allocation, which means
|
| 347 | * a more complete test that would include copy_from_user() would risk
|
| 348 | * memory corruption. Just test copy_to_user() here, as that exercises
|
| 349 | * almost exactly the same code paths.
|
| 350 |
|
| 351 | static void do_usercopy_page_span(const char *name, void *kaddr)
|
| 352 | {
|
| 353 | unsigned long uaddr;
|
| 354 |
|
| 355 | uaddr = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_WRITE,
|
| 356 | MAP_ANONYMOUS | MAP_PRIVATE, 0);
|
| 357 | if (uaddr >= TASK_SIZE) {
|
| 358 | pr_warn("Failed to allocate user memory\n");
|
| 359 | return;
|
| 360 | }
|
| 361 |
|
| 362 |
|
| 363 | memset(kaddr, 0xAA, PAGE_SIZE);
|
| 364 |
|
| 365 |
|
| 366 | kaddr += PAGE_SIZE / 2;
|
| 367 |
|
| 368 | pr_info("attempting good copy_to_user() from kernel %s: %px\n",
|
| 369 | name, kaddr);
|
| 370 | if (copy_to_user((void __user *)uaddr, kaddr,
|
| 371 | unconst + (PAGE_SIZE / 2))) {
|
| 372 | pr_err("copy_to_user() failed unexpectedly?!\n");
|
| 373 | goto free_user;
|
| 374 | }
|
| 375 |
|
| 376 | pr_info("attempting bad copy_to_user() from kernel %s: %px\n",
|
| 377 | name, kaddr);
|
| 378 | if (copy_to_user((void __user *)uaddr, kaddr, unconst + PAGE_SIZE)) {
|
| 379 | pr_warn("Good, copy_to_user() failed, but lacked Oops(?!)\n");
|
| 380 | goto free_user;
|
| 381 | }
|
| 382 |
|
| 383 | pr_err("FAIL: bad copy_to_user() not detected!\n");
|
| 384 | pr_expected_config_param(CONFIG_HARDENED_USERCOPY, "hardened_usercopy");
|
| 385 |
|
| 386 | free_user:
|
| 387 | vm_munmap(uaddr, PAGE_SIZE);
|
| 388 | }
|
| 389 |
|
| 390 | static void lkdtm_USERCOPY_VMALLOC(void)
|
| 391 | {
|
| 392 | void *addr;
|
| 393 |
|
| 394 | addr = vmalloc(PAGE_SIZE);
|
| 395 | if (!addr) {
|
| 396 | pr_err("vmalloc() failed!?\n");
|
| 397 | return;
|
| 398 | }
|
| 399 | do_usercopy_page_span("vmalloc", addr);
|
| 400 | vfree(addr);
|
| 401 | }
|
| 402 |
|
| 403 | static void lkdtm_USERCOPY_FOLIO(void)
|
| 404 | {
|
| 405 | struct folio *folio;
|
| 406 | void *addr;
|
| 407 |
|
| 408 | |
| 409 | * FIXME: Folio checking currently misses 0-order allocations, so
|
| 410 | * allocate and bump forward to the last page.
|
| 411 |
|
| 412 | folio = folio_alloc(GFP_KERNEL | __GFP_ZERO, 1);
|
| 413 | if (!folio) {
|
| 414 | pr_err("folio_alloc() failed!?\n");
|
| 415 | return;
|
| 416 | }
|
| 417 | addr = folio_address(folio);
|
| 418 | if (addr)
|
| 419 | do_usercopy_page_span("folio", addr + PAGE_SIZE);
|
| 420 | else
|
| 421 | pr_err("folio_address() failed?!\n");
|
| 422 | folio_put(folio);
|
| 423 | }
|
| 424 |
|
| 425 | void __init lkdtm_usercopy_init(void)
|
| 426 | {
|
| 427 |
|
| 428 | whitelist_cache =
|
| 429 | kmem_cache_create_usercopy("lkdtm-usercopy", cache_size,
|
| 430 | 0, 0,
|
| 431 | cache_size / 4,
|
| 432 | cache_size / 16,
|
| 433 | NULL);
|
| 434 | }
|
| 435 |
|
| 436 | void __exit lkdtm_usercopy_exit(void)
|
| 437 | {
|
| 438 | kmem_cache_destroy(whitelist_cache);
|
| 439 | }
|
| 440 |
|
| 441 | static struct crashtype crashtypes[] = {
|
| 442 | CRASHTYPE(USERCOPY_SLAB_SIZE_TO),
|
| 443 | CRASHTYPE(USERCOPY_SLAB_SIZE_FROM),
|
| 444 | CRASHTYPE(USERCOPY_SLAB_WHITELIST_TO),
|
| 445 | CRASHTYPE(USERCOPY_SLAB_WHITELIST_FROM),
|
| 446 | CRASHTYPE(USERCOPY_STACK_FRAME_TO),
|
| 447 | CRASHTYPE(USERCOPY_STACK_FRAME_FROM),
|
| 448 | CRASHTYPE(USERCOPY_STACK_BEYOND),
|
| 449 | CRASHTYPE(USERCOPY_VMALLOC),
|
| 450 | CRASHTYPE(USERCOPY_FOLIO),
|
| 451 | CRASHTYPE(USERCOPY_KERNEL),
|
| 452 | };
|
| 453 |
|
| 454 | struct crashtype_category usercopy_crashtypes = {
|
| 455 | .crashtypes = crashtypes,
|
| 456 | .len = ARRAY_SIZE(crashtypes),
|
| 457 | };
|