1#!/usr/bin/env perl 2# SPDX-License-Identifier: GPL-2.0-only 3# 4# (c) 2017 Tobin C. Harding <me@tobin.cc> 5# 6# leaking_addresses.pl: Scan the kernel for potential leaking addresses. 7# - Scans dmesg output. 8# - Walks directory tree and parses each file (for each directory in @DIRS). 9# 10# Use --debug to output path before parsing, this is useful to find files that 11# cause the script to choke. 12 13# 14# When the system is idle it is likely that most files under /proc/PID will be 15# identical for various processes. Scanning _all_ the PIDs under /proc is 16# unnecessary and implies that we are thoroughly scanning /proc. This is _not_ 17# the case because there may be ways userspace can trigger creation of /proc 18# files that leak addresses but were not present during a scan. For these two 19# reasons we exclude all PID directories under /proc except '1/' 20 21use warnings; 22use strict; 23use POSIX; 24use File::Basename; 25use File::Spec; 26use Cwd 'abs_path'; 27use Term::ANSIColor qw(:constants); 28use Getopt::Long qw(:config no_auto_abbrev); 29use Config; 30use bigint qw/hex/; 31use feature 'state'; 32 33my $P = $0; 34 35# Directories to scan. 36my @DIRS = ('/proc', '/sys'); 37 38# Timer for parsing each file, in seconds. 39my $TIMEOUT = 10; 40 41# Kernel addresses vary by architecture. We can only auto-detect the following 42# architectures (using `uname -m`). (flag --32-bit overrides auto-detection.) 43my @SUPPORTED_ARCHITECTURES = ('x86_64', 'ppc64', 'x86'); 44 45# Command line options. 46my $help = 0; 47my $debug = 0; 48my $raw = 0; 49my $output_raw = ""; # Write raw results to file. 50my $input_raw = ""; # Read raw results from file instead of scanning. 51my $suppress_dmesg = 0; # Don't show dmesg in output. 52my $squash_by_path = 0; # Summary report grouped by absolute path. 53my $squash_by_filename = 0; # Summary report grouped by filename. 54my $kernel_config_file = ""; # Kernel configuration file. 55my $opt_32bit = 0; # Scan 32-bit kernel. 56my $page_offset_32bit = 0; # Page offset for 32-bit kernel. 57 58# Skip these absolute paths. 59my @skip_abs = ( 60 '/proc/kmsg', 61 '/proc/device-tree', 62 '/proc/1/syscall', 63 '/sys/firmware/devicetree', 64 '/sys/kernel/tracing/trace_pipe', 65 '/sys/kernel/debug/tracing/trace_pipe', 66 '/sys/kernel/security/apparmor/revision'); 67 68# Skip these under any subdirectory. 69my @skip_any = ( 70 'pagemap', 71 'events', 72 'access', 73 'registers', 74 'snapshot_raw', 75 'trace_pipe_raw', 76 'ptmx', 77 'trace_pipe', 78 'fd', 79 'usbmon'); 80 81sub help 82{ 83 my ($exitcode) = @_; 84 85 print << "EOM"; 86 87Usage: $P [OPTIONS] 88 89Options: 90 91 -o, --output-raw=<file> Save results for future processing. 92 -i, --input-raw=<file> Read results from file instead of scanning. 93 --raw Show raw results (default). 94 --suppress-dmesg Do not show dmesg results. 95 --squash-by-path Show one result per unique path. 96 --squash-by-filename Show one result per unique filename. 97 --kernel-config-file=<file> Kernel configuration file (e.g /boot/config) 98 --32-bit Scan 32-bit kernel. 99 --page-offset-32-bit=o Page offset (for 32-bit kernel 0xABCD1234). 100 -d, --debug Display debugging output. 101 -h, --help Display this help and exit. 102 103Scans the running kernel for potential leaking addresses. 104 105EOM 106 exit($exitcode); 107} 108 109GetOptions( 110 'd|debug' => \$debug, 111 'h|help' => \$help, 112 'o|output-raw=s' => \$output_raw, 113 'i|input-raw=s' => \$input_raw, 114 'suppress-dmesg' => \$suppress_dmesg, 115 'squash-by-path' => \$squash_by_path, 116 'squash-by-filename' => \$squash_by_filename, 117 'raw' => \$raw, 118 'kernel-config-file=s' => \$kernel_config_file, 119 '32-bit' => \$opt_32bit, 120 'page-offset-32-bit=o' => \$page_offset_32bit, 121) or help(1); 122 123help(0) if ($help); 124 125if ($input_raw) { 126 format_output($input_raw); 127 exit(0); 128} 129 130if (!$input_raw and ($squash_by_path or $squash_by_filename)) { 131 printf "\nSummary reporting only available with --input-raw=<file>\n"; 132 printf "(First run scan with --output-raw=<file>.)\n"; 133 exit(128); 134} 135 136if (!(is_supported_architecture() or $opt_32bit or $page_offset_32bit)) { 137 printf "\nScript does not support your architecture, sorry.\n"; 138 printf "\nCurrently we support: \n\n"; 139 foreach(@SUPPORTED_ARCHITECTURES) { 140 printf "\t%s\n", $_; 141 } 142 printf("\n"); 143 144 printf("If you are running a 32-bit architecture you may use:\n"); 145 printf("\n\t--32-bit or --page-offset-32-bit=<page offset>\n\n"); 146 147 my $archname = `uname -m`; 148 printf("Machine hardware name (`uname -m`): %s\n", $archname); 149 150 exit(129); 151} 152 153if ($output_raw) { 154 open my $fh, '>', $output_raw or die "$0: $output_raw: $!\n"; 155 select $fh; 156} 157 158parse_dmesg(); 159walk(@DIRS); 160 161exit 0; 162 163sub dprint 164{ 165 printf(STDERR @_) if $debug; 166} 167 168sub is_supported_architecture 169{ 170 return (is_x86_64() or is_ppc64() or is_ix86_32()); 171} 172 173sub is_32bit 174{ 175 # Allow --32-bit or --page-offset-32-bit to override 176 if ($opt_32bit or $page_offset_32bit) { 177 return 1; 178 } 179 180 return is_ix86_32(); 181} 182 183sub is_ix86_32 184{ 185 state $arch = `uname -m`; 186 187 chomp $arch; 188 if ($arch =~ m/i[3456]86/) { 189 return 1; 190 } 191 return 0; 192} 193 194sub is_arch 195{ 196 my ($desc) = @_; 197 my $arch = `uname -m`; 198 199 chomp $arch; 200 if ($arch eq $desc) { 201 return 1; 202 } 203 return 0; 204} 205 206sub is_x86_64 207{ 208 state $is = is_arch('x86_64'); 209 return $is; 210} 211 212sub is_ppc64 213{ 214 state $is = is_arch('ppc64'); 215 return $is; 216} 217 218# Gets config option value from kernel config file. 219# Returns "" on error or if config option not found. 220sub get_kernel_config_option 221{ 222 my ($option) = @_; 223 my $value = ""; 224 my $tmp_file = ""; 225 my @config_files; 226 227 # Allow --kernel-config-file to override. 228 if ($kernel_config_file ne "") { 229 @config_files = ($kernel_config_file); 230 } elsif (-R "/proc/config.gz") { 231 my $tmp_file = "/tmp/tmpkconf"; 232 233 if (system("gunzip < /proc/config.gz > $tmp_file")) { 234 dprint("system(gunzip < /proc/config.gz) failed\n"); 235 return ""; 236 } else { 237 @config_files = ($tmp_file); 238 } 239 } else { 240 my $file = '/boot/config-' . `uname -r`; 241 chomp $file; 242 @config_files = ($file, '/boot/config'); 243 } 244 245 foreach my $file (@config_files) { 246 dprint("parsing config file: $file\n"); 247 $value = option_from_file($option, $file); 248 if ($value ne "") { 249 last; 250 } 251 } 252 253 if ($tmp_file ne "") { 254 system("rm -f $tmp_file"); 255 } 256 257 return $value; 258} 259 260# Parses $file and returns kernel configuration option value. 261sub option_from_file 262{ 263 my ($option, $file) = @_; 264 my $str = ""; 265 my $val = ""; 266 267 open(my $fh, "<", $file) or return ""; 268 while (my $line = <$fh> ) { 269 if ($line =~ /^$option/) { 270 ($str, $val) = split /=/, $line; 271 chomp $val; 272 last; 273 } 274 } 275 276 close $fh; 277 return $val; 278} 279 280sub is_false_positive 281{ 282 my ($match) = @_; 283 284 if (is_32bit()) { 285 return is_false_positive_32bit($match); 286 } 287 288 # 64 bit false positives. 289 290 if ($match =~ '\b(0x)?(f|F){16}\b' or 291 $match =~ '\b(0x)?0{16}\b') { 292 return 1; 293 } 294 295 if (is_x86_64() and is_in_vsyscall_memory_region($match)) { 296 return 1; 297 } 298 299 return 0; 300} 301 302sub is_false_positive_32bit 303{ 304 my ($match) = @_; 305 state $page_offset = get_page_offset(); 306 307 if ($match =~ '\b(0x)?(f|F){8}\b') { 308 return 1; 309 } 310 311 if (hex($match) < $page_offset) { 312 return 1; 313 } 314 315 return 0; 316} 317 318# returns integer value 319sub get_page_offset 320{ 321 my $page_offset; 322 my $default_offset = 0xc0000000; 323 324 # Allow --page-offset-32bit to override. 325 if ($page_offset_32bit != 0) { 326 return $page_offset_32bit; 327 } 328 329 $page_offset = get_kernel_config_option('CONFIG_PAGE_OFFSET'); 330 if (!$page_offset) { 331 return $default_offset; 332 } 333 return $page_offset; 334} 335 336sub is_in_vsyscall_memory_region 337{ 338 my ($match) = @_; 339 340 my $hex = hex($match); 341 my $region_min = hex("0xffffffffff600000"); 342 my $region_max = hex("0xffffffffff601000"); 343 344 return ($hex >= $region_min and $hex <= $region_max); 345} 346 347# True if argument potentially contains a kernel address. 348sub may_leak_address 349{ 350 my ($line) = @_; 351 my $address_re; 352 353 # Signal masks. 354 if ($line =~ '^SigBlk:' or 355 $line =~ '^SigIgn:' or 356 $line =~ '^SigCgt:') { 357 return 0; 358 } 359 360 if ($line =~ '\bKEY=[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' or 361 $line =~ '\b[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b') { 362 return 0; 363 } 364 365 $address_re = get_address_re(); 366 while ($line =~ /($address_re)/g) { 367 if (!is_false_positive($1)) { 368 return 1; 369 } 370 } 371 372 return 0; 373} 374 375sub get_address_re 376{ 377 if (is_ppc64()) { 378 return '\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b'; 379 } elsif (is_32bit()) { 380 return '\b(0x)?[[:xdigit:]]{8}\b'; 381 } 382 383 return get_x86_64_re(); 384} 385 386sub get_x86_64_re 387{ 388 # We handle page table levels but only if explicitly configured using 389 # CONFIG_PGTABLE_LEVELS. If config file parsing fails or config option 390 # is not found we default to using address regular expression suitable 391 # for 4 page table levels. 392 state $ptl = get_kernel_config_option('CONFIG_PGTABLE_LEVELS'); 393 394 if ($ptl == 5) { 395 return '\b(0x)?ff[[:xdigit:]]{14}\b'; 396 } 397 return '\b(0x)?ffff[[:xdigit:]]{12}\b'; 398} 399 400sub parse_dmesg 401{ 402 open my $cmd, '-|', 'dmesg'; 403 while (<$cmd>) { 404 if (may_leak_address($_)) { 405 print 'dmesg: ' . $_; 406 } 407 } 408 close $cmd; 409} 410 411# True if we should skip this path. 412sub skip 413{ 414 my ($path) = @_; 415 416 foreach (@skip_abs) { 417 return 1 if (/^$path$/); 418 } 419 420 my($filename, $dirs, $suffix) = fileparse($path); 421 foreach (@skip_any) { 422 return 1 if (/^$filename$/); 423 } 424 425 return 0; 426} 427 428sub timed_parse_file 429{ 430 my ($file) = @_; 431 432 eval { 433 local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required. 434 alarm $TIMEOUT; 435 parse_file($file); 436 alarm 0; 437 }; 438 439 if ($@) { 440 die unless $@ eq "alarm\n"; # Propagate unexpected errors. 441 printf STDERR "timed out parsing: %s\n", $file; 442 } 443} 444 445sub parse_file 446{ 447 my ($file) = @_; 448 449 if (! -R $file) { 450 return; 451 } 452 453 if (! -T $file) { 454 return; 455 } 456 457 open my $fh, "<", $file or return; 458 while ( <$fh> ) { 459 chomp; 460 if (may_leak_address($_)) { 461 printf("$file: $_\n"); 462 } 463 } 464 close $fh; 465} 466 467# Checks if the actual path name is leaking a kernel address. 468sub check_path_for_leaks 469{ 470 my ($path) = @_; 471 472 if (may_leak_address($path)) { 473 printf("Path name may contain address: $path\n"); 474 } 475} 476 477# Recursively walk directory tree. 478sub walk 479{ 480 my @dirs = @_; 481 482 while (my $pwd = shift @dirs) { 483 next if (!opendir(DIR, $pwd)); 484 my @files = readdir(DIR); 485 closedir(DIR); 486 487 foreach my $file (@files) { 488 next if ($file eq '.' or $file eq '..'); 489 490 my $path = "$pwd/$file"; 491 next if (-l $path); 492 493 # skip /proc/PID except /proc/1 494 next if (($path =~ /^\/proc\/[0-9]+$/) && 495 ($path !~ /^\/proc\/1$/)); 496 497 next if (skip($path)); 498 499 check_path_for_leaks($path); 500 501 if (-d $path) { 502 push @dirs, $path; 503 next; 504 } 505 506 dprint("parsing: $path\n"); 507 timed_parse_file($path); 508 } 509 } 510} 511 512sub format_output 513{ 514 my ($file) = @_; 515 516 # Default is to show raw results. 517 if ($raw or (!$squash_by_path and !$squash_by_filename)) { 518 dump_raw_output($file); 519 return; 520 } 521 522 my ($total, $dmesg, $paths, $files) = parse_raw_file($file); 523 524 printf "\nTotal number of results from scan (incl dmesg): %d\n", $total; 525 526 if (!$suppress_dmesg) { 527 print_dmesg($dmesg); 528 } 529 530 if ($squash_by_filename) { 531 squash_by($files, 'filename'); 532 } 533 534 if ($squash_by_path) { 535 squash_by($paths, 'path'); 536 } 537} 538 539sub dump_raw_output 540{ 541 my ($file) = @_; 542 543 open (my $fh, '<', $file) or die "$0: $file: $!\n"; 544 while (<$fh>) { 545 if ($suppress_dmesg) { 546 if ("dmesg:" eq substr($_, 0, 6)) { 547 next; 548 } 549 } 550 print $_; 551 } 552 close $fh; 553} 554 555sub parse_raw_file 556{ 557 my ($file) = @_; 558 559 my $total = 0; # Total number of lines parsed. 560 my @dmesg; # dmesg output. 561 my %files; # Unique filenames containing leaks. 562 my %paths; # Unique paths containing leaks. 563 564 open (my $fh, '<', $file) or die "$0: $file: $!\n"; 565 while (my $line = <$fh>) { 566 $total++; 567 568 if ("dmesg:" eq substr($line, 0, 6)) { 569 push @dmesg, $line; 570 next; 571 } 572 573 cache_path(\%paths, $line); 574 cache_filename(\%files, $line); 575 } 576 577 return $total, \@dmesg, \%paths, \%files; 578} 579 580sub print_dmesg 581{ 582 my ($dmesg) = @_; 583 584 print "\ndmesg output:\n"; 585 586 if (@$dmesg == 0) { 587 print "<no results>\n"; 588 return; 589 } 590 591 foreach(@$dmesg) { 592 my $index = index($_, ': '); 593 $index += 2; # skid ': ' 594 print substr($_, $index); 595 } 596} 597 598sub squash_by 599{ 600 my ($ref, $desc) = @_; 601 602 print "\nResults squashed by $desc (excl dmesg). "; 603 print "Displaying [<number of results> <$desc>], <example result>\n"; 604 605 if (keys %$ref == 0) { 606 print "<no results>\n"; 607 return; 608 } 609 610 foreach(keys %$ref) { 611 my $lines = $ref->{$_}; 612 my $length = @$lines; 613 printf "[%d %s] %s", $length, $_, @$lines[0]; 614 } 615} 616 617sub cache_path 618{ 619 my ($paths, $line) = @_; 620 621 my $index = index($line, ': '); 622 my $path = substr($line, 0, $index); 623 624 $index += 2; # skip ': ' 625 add_to_cache($paths, $path, substr($line, $index)); 626} 627 628sub cache_filename 629{ 630 my ($files, $line) = @_; 631 632 my $index = index($line, ': '); 633 my $path = substr($line, 0, $index); 634 my $filename = basename($path); 635 636 $index += 2; # skip ': ' 637 add_to_cache($files, $filename, substr($line, $index)); 638} 639 640sub add_to_cache 641{ 642 my ($cache, $key, $value) = @_; 643 644 if (!$cache->{$key}) { 645 $cache->{$key} = (); 646 } 647 push @{$cache->{$key}}, $value; 648} 649