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