]> asedeno.scripts.mit.edu Git - linux.git/blob - scripts/leaking_addresses.pl
leaking_addresses: remove command examples
[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
23 my $P = $0;
24 my $V = '0.01';
25
26 # Directories to scan.
27 my @DIRS = ('/proc', '/sys');
28
29 # Timer for parsing each file, in seconds.
30 my $TIMEOUT = 10;
31
32 # Script can only grep for kernel addresses on the following architectures. If
33 # your architecture is not listed here and has a grep'able kernel address please
34 # consider submitting a patch.
35 my @SUPPORTED_ARCHITECTURES = ('x86_64', 'ppc64');
36
37 # Command line options.
38 my $help = 0;
39 my $debug = 0;
40 my $raw = 0;
41 my $output_raw = "";    # Write raw results to file.
42 my $input_raw = "";     # Read raw results from file instead of scanning.
43
44 my $suppress_dmesg = 0;         # Don't show dmesg in output.
45 my $squash_by_path = 0;         # Summary report grouped by absolute path.
46 my $squash_by_filename = 0;     # Summary report grouped by filename.
47
48 # Do not parse these files (absolute path).
49 my @skip_parse_files_abs = ('/proc/kmsg',
50                             '/proc/kcore',
51                             '/proc/fs/ext4/sdb1/mb_groups',
52                             '/proc/1/fd/3',
53                             '/sys/firmware/devicetree',
54                             '/proc/device-tree',
55                             '/sys/kernel/debug/tracing/trace_pipe',
56                             '/sys/kernel/security/apparmor/revision');
57
58 # Do not parse these files under any subdirectory.
59 my @skip_parse_files_any = ('0',
60                             '1',
61                             '2',
62                             'pagemap',
63                             'events',
64                             'access',
65                             'registers',
66                             'snapshot_raw',
67                             'trace_pipe_raw',
68                             'ptmx',
69                             'trace_pipe');
70
71 # Do not walk these directories (absolute path).
72 my @skip_walk_dirs_abs = ();
73
74 # Do not walk these directories under any subdirectory.
75 my @skip_walk_dirs_any = ('self',
76                           'thread-self',
77                           'cwd',
78                           'fd',
79                           'usbmon',
80                           'stderr',
81                           'stdin',
82                           'stdout');
83
84 sub help
85 {
86         my ($exitcode) = @_;
87
88         print << "EOM";
89
90 Usage: $P [OPTIONS]
91 Version: $V
92
93 Options:
94
95         -o, --output-raw=<file>  Save results for future processing.
96         -i, --input-raw=<file>   Read results from file instead of scanning.
97             --raw                Show raw results (default).
98             --suppress-dmesg     Do not show dmesg results.
99             --squash-by-path     Show one result per unique path.
100             --squash-by-filename Show one result per unique filename.
101         -d, --debug              Display debugging output.
102         -h, --help, --version    Display this help and exit.
103
104 Scans the running (64 bit) kernel for potential leaking addresses.
105
106 EOM
107         exit($exitcode);
108 }
109
110 GetOptions(
111         'd|debug'               => \$debug,
112         'h|help'                => \$help,
113         'version'               => \$help,
114         'o|output-raw=s'        => \$output_raw,
115         'i|input-raw=s'         => \$input_raw,
116         'suppress-dmesg'        => \$suppress_dmesg,
117         'squash-by-path'        => \$squash_by_path,
118         'squash-by-filename'    => \$squash_by_filename,
119         'raw'                   => \$raw,
120 ) or help(1);
121
122 help(0) if ($help);
123
124 if ($input_raw) {
125         format_output($input_raw);
126         exit(0);
127 }
128
129 if (!$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
135 if (!is_supported_architecture()) {
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
142         my $archname = $Config{archname};
143         printf "\n\$ perl -MConfig -e \'print \"\$Config{archname}\\n\"\'\n";
144         printf "%s\n", $archname;
145
146         exit(129);
147 }
148
149 if ($output_raw) {
150         open my $fh, '>', $output_raw or die "$0: $output_raw: $!\n";
151         select $fh;
152 }
153
154 parse_dmesg();
155 walk(@DIRS);
156
157 exit 0;
158
159 sub dprint
160 {
161         printf(STDERR @_) if $debug;
162 }
163
164 sub is_supported_architecture
165 {
166         return (is_x86_64() or is_ppc64());
167 }
168
169 sub is_x86_64
170 {
171         my $archname = $Config{archname};
172
173         if ($archname =~ m/x86_64/) {
174                 return 1;
175         }
176         return 0;
177 }
178
179 sub is_ppc64
180 {
181         my $archname = $Config{archname};
182
183         if ($archname =~ m/powerpc/ and $archname =~ m/64/) {
184                 return 1;
185         }
186         return 0;
187 }
188
189 sub is_false_positive
190 {
191         my ($match) = @_;
192
193         if ($match =~ '\b(0x)?(f|F){16}\b' or
194             $match =~ '\b(0x)?0{16}\b') {
195                 return 1;
196         }
197
198         if (is_x86_64()) {
199                 # vsyscall memory region, we should probably check against a range here.
200                 if ($match =~ '\bf{10}600000\b' or
201                     $match =~ '\bf{10}601000\b') {
202                         return 1;
203                 }
204         }
205
206         return 0;
207 }
208
209 # True if argument potentially contains a kernel address.
210 sub may_leak_address
211 {
212         my ($line) = @_;
213         my $address_re;
214
215         # Signal masks.
216         if ($line =~ '^SigBlk:' or
217             $line =~ '^SigIgn:' or
218             $line =~ '^SigCgt:') {
219                 return 0;
220         }
221
222         if ($line =~ '\bKEY=[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' or
223             $line =~ '\b[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b') {
224                 return 0;
225         }
226
227         # One of these is guaranteed to be true.
228         if (is_x86_64()) {
229                 $address_re = '\b(0x)?ffff[[:xdigit:]]{12}\b';
230         } elsif (is_ppc64()) {
231                 $address_re = '\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b';
232         }
233
234         while (/($address_re)/g) {
235                 if (!is_false_positive($1)) {
236                         return 1;
237                 }
238         }
239
240         return 0;
241 }
242
243 sub parse_dmesg
244 {
245         open my $cmd, '-|', 'dmesg';
246         while (<$cmd>) {
247                 if (may_leak_address($_)) {
248                         print 'dmesg: ' . $_;
249                 }
250         }
251         close $cmd;
252 }
253
254 # True if we should skip this path.
255 sub skip
256 {
257         my ($path, $paths_abs, $paths_any) = @_;
258
259         foreach (@$paths_abs) {
260                 return 1 if (/^$path$/);
261         }
262
263         my($filename, $dirs, $suffix) = fileparse($path);
264         foreach (@$paths_any) {
265                 return 1 if (/^$filename$/);
266         }
267
268         return 0;
269 }
270
271 sub skip_parse
272 {
273         my ($path) = @_;
274         return skip($path, \@skip_parse_files_abs, \@skip_parse_files_any);
275 }
276
277 sub timed_parse_file
278 {
279         my ($file) = @_;
280
281         eval {
282                 local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required.
283                 alarm $TIMEOUT;
284                 parse_file($file);
285                 alarm 0;
286         };
287
288         if ($@) {
289                 die unless $@ eq "alarm\n";     # Propagate unexpected errors.
290                 printf STDERR "timed out parsing: %s\n", $file;
291         }
292 }
293
294 sub parse_file
295 {
296         my ($file) = @_;
297
298         if (! -R $file) {
299                 return;
300         }
301
302         if (skip_parse($file)) {
303                 dprint "skipping file: $file\n";
304                 return;
305         }
306         dprint "parsing: $file\n";
307
308         open my $fh, "<", $file or return;
309         while ( <$fh> ) {
310                 if (may_leak_address($_)) {
311                         print $file . ': ' . $_;
312                 }
313         }
314         close $fh;
315 }
316
317
318 # True if we should skip walking this directory.
319 sub skip_walk
320 {
321         my ($path) = @_;
322         return skip($path, \@skip_walk_dirs_abs, \@skip_walk_dirs_any)
323 }
324
325 # Recursively walk directory tree.
326 sub walk
327 {
328         my @dirs = @_;
329
330         while (my $pwd = shift @dirs) {
331                 next if (skip_walk($pwd));
332                 next if (!opendir(DIR, $pwd));
333                 my @files = readdir(DIR);
334                 closedir(DIR);
335
336                 foreach my $file (@files) {
337                         next if ($file eq '.' or $file eq '..');
338
339                         my $path = "$pwd/$file";
340                         next if (-l $path);
341
342                         if (-d $path) {
343                                 push @dirs, $path;
344                         } else {
345                                 timed_parse_file($path);
346                         }
347                 }
348         }
349 }
350
351 sub format_output
352 {
353         my ($file) = @_;
354
355         # Default is to show raw results.
356         if ($raw or (!$squash_by_path and !$squash_by_filename)) {
357                 dump_raw_output($file);
358                 return;
359         }
360
361         my ($total, $dmesg, $paths, $files) = parse_raw_file($file);
362
363         printf "\nTotal number of results from scan (incl dmesg): %d\n", $total;
364
365         if (!$suppress_dmesg) {
366                 print_dmesg($dmesg);
367         }
368
369         if ($squash_by_filename) {
370                 squash_by($files, 'filename');
371         }
372
373         if ($squash_by_path) {
374                 squash_by($paths, 'path');
375         }
376 }
377
378 sub dump_raw_output
379 {
380         my ($file) = @_;
381
382         open (my $fh, '<', $file) or die "$0: $file: $!\n";
383         while (<$fh>) {
384                 if ($suppress_dmesg) {
385                         if ("dmesg:" eq substr($_, 0, 6)) {
386                                 next;
387                         }
388                 }
389                 print $_;
390         }
391         close $fh;
392 }
393
394 sub parse_raw_file
395 {
396         my ($file) = @_;
397
398         my $total = 0;          # Total number of lines parsed.
399         my @dmesg;              # dmesg output.
400         my %files;              # Unique filenames containing leaks.
401         my %paths;              # Unique paths containing leaks.
402
403         open (my $fh, '<', $file) or die "$0: $file: $!\n";
404         while (my $line = <$fh>) {
405                 $total++;
406
407                 if ("dmesg:" eq substr($line, 0, 6)) {
408                         push @dmesg, $line;
409                         next;
410                 }
411
412                 cache_path(\%paths, $line);
413                 cache_filename(\%files, $line);
414         }
415
416         return $total, \@dmesg, \%paths, \%files;
417 }
418
419 sub print_dmesg
420 {
421         my ($dmesg) = @_;
422
423         print "\ndmesg output:\n";
424
425         if (@$dmesg == 0) {
426                 print "<no results>\n";
427                 return;
428         }
429
430         foreach(@$dmesg) {
431                 my $index = index($_, ': ');
432                 $index += 2;    # skid ': '
433                 print substr($_, $index);
434         }
435 }
436
437 sub squash_by
438 {
439         my ($ref, $desc) = @_;
440
441         print "\nResults squashed by $desc (excl dmesg). ";
442         print "Displaying [<number of results> <$desc>], <example result>\n";
443
444         if (keys %$ref == 0) {
445                 print "<no results>\n";
446                 return;
447         }
448
449         foreach(keys %$ref) {
450                 my $lines = $ref->{$_};
451                 my $length = @$lines;
452                 printf "[%d %s] %s", $length, $_, @$lines[0];
453         }
454 }
455
456 sub cache_path
457 {
458         my ($paths, $line) = @_;
459
460         my $index = index($line, ': ');
461         my $path = substr($line, 0, $index);
462
463         $index += 2;            # skip ': '
464         add_to_cache($paths, $path, substr($line, $index));
465 }
466
467 sub cache_filename
468 {
469         my ($files, $line) = @_;
470
471         my $index = index($line, ': ');
472         my $path = substr($line, 0, $index);
473         my $filename = basename($path);
474
475         $index += 2;            # skip ': '
476         add_to_cache($files, $filename, substr($line, $index));
477 }
478
479 sub add_to_cache
480 {
481         my ($cache, $key, $value) = @_;
482
483         if (!$cache->{$key}) {
484                 $cache->{$key} = ();
485         }
486         push @{$cache->{$key}}, $value;
487 }