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