MacOS: panic decoder should check for unloaded kexts
[openafs.git] / src / packaging / MacOS / decode-panic
1 #!/usr/bin/perl
2
3 # decode-panic - decode a Mac OS panic log to show source line numbers
4 # see the end of the file for full documentation and license.
5
6 use Carp;
7 use English qw( -no_match_vars ) ;
8 use File::Basename;
9 use File::Temp qw( tempdir );
10 use Getopt::Long;
11 use IO::Dir;
12 use IO::File;
13 use Pod::Usage;
14 use warnings;
15 use strict;
16
17 my $panic_file = "/Library/Logs/panic.log";
18 my %crash_info;
19 my $backtrace;
20 my $debugkit = "";
21 my $archive = "";
22 my $dmgutil = "";
23 my $kextload   = "/sbin/kextload";
24 my $kextutil   = "/usr/bin/kextutil";
25 my $kextprog;
26 my $gdb        = "/usr/bin/gdb";
27 my $gdbarch    = "";
28 my $kextarch   = "";
29 my $gdb_file   = "gdb.input";
30 my $temp_dir   = tempdir( "afsdebugXXXXXX", DIR => File::Spec->tmpdir,
31                           TMPDIR => 1, CLEANUP => 1 );
32 my $dump_file  = "/var/db/openafs/logs/crash.dump";
33 my $kernel = "/mach_kernel";
34 my $kextpath = "/Library/OpenAFS/Tools/root.client/usr/vice/etc/afs.kext/";
35
36 my $option_quiet;
37 my $option_verbose;
38 my $option_help;
39 my $result = GetOptions ("input=s"  => \$panic_file,
40                          "output=s" => \$dump_file,
41                          "kernel=s" => \$kernel,
42                          "debugkit=s" => \$debugkit,
43                          "archive=s"=> \$archive,
44                          "util=s"   => \$dmgutil,
45                          "verbose"  => \$option_verbose,
46                          "quiet"    => \$option_quiet,
47                          "help"     => \$option_help
48                      );
49
50 if ( !$result ) {
51     pod2usage(-message => "Syntax error.",
52               -exitval => 2,
53               -verbose => 1,
54               -output  => \*STDERR);
55     
56     exit;
57 }
58
59 if ($option_help) {
60     pod2usage(-message => "",
61               -exitval => 2,
62               -verbose => 3,
63               -output  => \*STDERR);
64     exit;
65 }
66
67 # check for necessary programs & panic file
68 for my $program ( $gdb, $kextload ) {
69     if ( ! -x $program ) {
70         if ( $option_quiet ) {
71             exit 1;
72         } else {
73             croak "Can't find $program!\n"
74         }
75     }
76 }
77
78 if ( -x $kextutil ) {
79     $kextprog = $kextutil;
80 } else {
81     $kextprog = $kextload;
82 }
83
84 croak "Can't find panic file: $panic_file!\n" if ( ! -r $panic_file );
85
86 $crash_info{"warning"} = "";
87
88 read_panic( $panic_file, \%crash_info );
89
90 if ($crash_info{"kernel_version"} =~ /X86_64/ ) {
91     $gdbarch="-a x86_64";
92     $kextarch="x86_64";
93 } else {
94     if ($crash_info{"kernel_version"} =~ /I386/ ) {
95         $gdbarch="-a i386";
96         $kextarch="i386";
97     } else {
98         if ($crash_info{"kernel_version"} =~ /PPC/ ) {
99             $gdbarch="-a ppc";
100             $kextarch="ppc";
101         }
102     }
103 }
104
105 if (-d $debugkit && -f $dmgutil ) {
106     extract_kernel( $crash_info{"kernel_version"}, $temp_dir, $debugkit, $dmgutil );
107     $kernel = "$temp_dir/mach_kernel";
108 }
109
110 if (-d $archive && -f $dmgutil ) {
111     extract_openafs( $crash_info{"afs_info"}, $temp_dir, $archive, $crash_info{"kernel_version"}, $dmgutil );
112     if (-d "$temp_dir/Library/OpenAFS/Debug/afs.kext" ) {
113         $kextpath = "$temp_dir/Library/OpenAFS/Debug/afs.kext";
114     } else {
115         $kextpath = "$temp_dir/Library/OpenAFS/Tools/root.client/usr/vice/etc/afs.kext";
116     }
117 }
118
119 generate_symbol_files( $crash_info{"afs_kernel_address"}, $temp_dir, $kextarch , $kernel, $kextpath);
120
121 write_gdb_input_file( $temp_dir, $gdb_file, $crash_info{ "backtrace" } );
122
123 if ($option_verbose) {
124     print "$gdb $gdbarch $kernel -batch -x $temp_dir/$gdb_file\n";
125 }
126 my $gdb_output = `$gdb $gdbarch $kernel -batch -x $temp_dir/$gdb_file`;
127 croak "gdb failed!\n" if $CHILD_ERROR;
128
129 write_dump_file( $dump_file, \%crash_info, $gdb_output );
130
131 sub extract_openafs {
132     my $oversion = shift;
133     my $tempdir = shift;
134     my $oarchive = shift;
135     my $kversion = shift;
136     my $hdutil = shift;
137
138     $kversion =~ /Darwin Kernel Version ([0-9]+).[0-9]+.[0-9]+:/;
139     my $major = $1;
140     $major -= 4;
141
142     $oversion =~ /org.openafs.filesystems.afs\(([0-9.]+)\)/;
143     my $vers = $1;
144     $vers =~ s/fc/pre/;
145     my $dmgvers = $vers;
146     if ($vers =~ /([0-9]+)f([0-9]+)/) {
147         $dmgvers = "$1" . "." . "$2";
148         $vers = "$1";
149     }
150
151     my $odmg = "$oarchive/$vers/macos-10.${major}/OpenAFS-$vers-\*.dmg";
152     if ($option_verbose) {
153         print "$hdutil $odmg extractall OpenAFS.pkg $tempdir/OpenAFS.pkg\n"; 
154     }
155     `$hdutil $odmg extractall OpenAFS.pkg $tempdir/OpenAFS.pkg`;
156     if ($option_verbose) {
157         print "cd $tempdir && gzcat $tempdir/OpenAFS.pkg/Contents/Archive.pax.gz | pax -r ./Library/OpenAFS/Tools/root.client/usr/vice/etc/afs.kext\n";
158     }
159     `cd $tempdir && gzcat $tempdir/OpenAFS.pkg/Contents/Archive.pax.gz | pax -r ./Library/OpenAFS/Tools/root.client/usr/vice/etc/afs.kext`;
160     if ($option_verbose) {
161         print "$hdutil $odmg extractall OpenAFS-debug-extension.pkg $tempdir/OpenAFS-debug-extension.pkg\n";
162     }
163     `$hdutil $odmg extractall OpenAFS-debug-extension.pkg $tempdir/OpenAFS-debug-extension.pkg`;
164     if (-f "$tempdir/OpenAFS-debug-extension.pkg/Contents/Archive.pax.gz" ) {
165         if ($option_verbose) {
166             print "cd $tempdir && gzcat $tempdir/OpenAFS-debug-extension.pkg/Contents/Archive.pax.gz | pax -r ./Library/OpenAFS/Debug/afs.kext.dSYM\n";
167         }
168         `cd $tempdir && gzcat $tempdir/OpenAFS-debug-extension.pkg/Contents/Archive.pax.gz | pax -r ./Library/OpenAFS/Debug/afs.kext.dSYM`;
169         if ($option_verbose) {
170             print "cd $tempdir && gzcat $tempdir/OpenAFS-debug-extension.pkg/Contents/Archive.pax.gz | pax -r ./Library/OpenAFS/Debug/afs.kext\n";
171         }
172         `cd $tempdir && gzcat $tempdir/OpenAFS-debug-extension.pkg/Contents/Archive.pax.gz | pax -r ./Library/OpenAFS/Debug/afs.kext`;
173     }
174 }
175
176 sub extract_kernel {
177     my $kversion = shift;
178     my $tempdir = shift;
179     my $debugarchive = shift;
180     my $hdutil = shift;
181     
182     $kversion =~ /Darwin Kernel Version ([0-9]+).([0-9]+).[0-9]+:/;
183     my $minor = $2;
184     my $major = $1;
185     $major -= 4;
186     my $kdk = "$debugarchive/kernel_debug_kit_10.${major}.${minor}_\*.dmg";
187     if ($option_verbose) {
188         print "$hdutil $kdk extractall System.kext $tempdir/System.kext\n";
189     }
190     `$hdutil $kdk extractall System.kext $tempdir/System.kext`;
191     if ($option_verbose) {
192         print "$hdutil $kdk extractall mach_kernel.dSYM $tempdir/mach_kernel.dSYM\n";
193     }
194     `$hdutil $kdk extractall mach_kernel.dSYM $tempdir/mach_kernel.dSYM`;
195     if ($option_verbose) {
196         print "$hdutil $kdk extract mach_kernel $tempdir/mach_kernel\n";
197     }
198     `$hdutil $kdk extract mach_kernel $tempdir/mach_kernel`;
199 }
200
201
202 # read the panic file and parse out the addresses
203 sub read_panic {
204
205     my $filename      = shift;
206     my $hash_ref      = shift;
207
208     my $kernel_line;
209     my $line;
210     my @panic_section_positions = ( 0 );
211
212     my $panic_fh = IO::File->new( $filename, '<' )
213         or croak "Can't open backtrace file $filename: $OS_ERROR\n";
214
215     # find the last panic section as denoted by "*********"
216     while ( $line = <$panic_fh> ) {
217         chomp $line;
218         if ( $line eq "*********" ) {
219             # skip a line
220             $line = <$panic_fh>;
221             push @panic_section_positions, $panic_fh->tell;
222         }
223     }
224
225     # ignore the empty last section
226     if ( @panic_section_positions > 2 ) {
227         pop @panic_section_positions
228     }
229
230     # Seek to last full panic section
231     # or the beginning of the file if appropriate
232     $panic_fh->seek( $panic_section_positions[-1], 0 );
233
234     $hash_ref->{ "date" } = <$panic_fh>;
235     chomp $hash_ref->{ "date" };
236
237     while ( $line = <$panic_fh> ) {
238         chomp $line;
239     
240         #skip lines until "Backtrace" is seen
241         $line =~ /^\s*(Backtrace,|Backtrace:|Backtrace \()/;
242         $backtrace = $1;
243         last if $backtrace;
244     }
245     
246     if ( !$backtrace ) {
247         if ( $option_quiet ) {
248             exit 1;
249         } else {
250             croak "Couldn't find a backtrace in $filename\n";
251         }
252     }
253     
254     # gather the backtrace addresses
255     if ( $backtrace eq "Backtrace:" ) {
256         # ppc format panic
257         while ( $line = <$panic_fh> ) {
258             chomp $line;
259             last if $line !~ /^\s*(0x[0-9a-fA-F]+)/;
260             my @memory_addresses = split /\s+/, $line;
261
262             # add non-empty array elements to the list
263             push @{ $hash_ref->{ "backtrace" } },
264                 grep { /0x/ } @memory_addresses;
265         }
266     } else {
267         # intel format panic
268         while ( $line = <$panic_fh> ) {
269             chomp $line;
270             last if $line !~ /^\s*0x[0-9a-fA-F]+ : (0x[0-9a-fA-F]*)/;
271             push @{ $hash_ref->{ "backtrace" } }, $1;
272         }
273     }
274
275     # now we need the address for the afs kernel module
276     while ( $line = <$panic_fh> ) {
277         chomp $line;
278         last if ($line =~ /^BSD\s+process/ );
279         next if ( $line !~ /org\.openafs\.filesystems\.afs/ );
280
281         $kernel_line = $line;
282         $line =~ /\@(0x[0-9a-fA-F]+)/;
283         $hash_ref->{ "afs_kernel_address" } = $1;
284         $kernel_line =~ /^\s+([^@]+)@.+/;
285         $hash_ref->{ "afs_info" } = $1;
286
287         last;
288     }
289
290     # grab the kernel version
291     while ( $line = <$panic_fh> ) {
292         chomp $line;
293         next if ( $line !~ /^Darwin Kernel Version/ );
294         $hash_ref->{ "kernel_version" } = $line;
295         last;
296     }
297     
298     if (! $kernel_line ) {
299         #unloaded?
300         while ( $line = <$panic_fh> ) {
301             chomp $line;
302             last if ( $line =~ /^loaded\s+kexts:/ );
303             next if ( $line !~ /org\.openafs\.filesystems\.afs/ );
304             $kernel_line = $line;
305             $line =~ /org\.openafs\.filesystems\.afs\s+([^@]+)\s+\(addr\s+(0x[0-9a-fA-F]+),/;
306             $hash_ref->{ "afs_kernel_address" } = $2;
307             $hash_ref->{ "afs_info" } = "org.openafs.filesystems.afs(" . $1 . ")\@0x" . $2;
308             $hash_ref->{ "warning" } = "MODULE WAS UNLOADED!\n";
309         }
310     }
311
312     $panic_fh->close()
313         or croak "Can't close file $filename: $OS_ERROR\n";
314     
315     if ( !$kernel_line ) {
316         if ( $option_quiet ) {
317             exit 1;
318         } else {
319             croak "No OpenAFS reference found in latest panic!";
320         }
321     }
322 }
323
324 # generate the symbol files that will be read by gdb
325 sub generate_symbol_files {
326     my $kernel_address   = shift;
327     my $symbol_write_dir = shift;
328     my $kextarch = shift;
329     my $kernel = shift;
330     my $kext = shift;
331
332     if ($kextprog eq $kextload) {
333         if ($kernel eq "/mach_kernel") {
334             if ($option_verbose) {
335                 print "$kextprog -k $kernel -s $temp_dir -a org.openafs.filesystems.afs\@${kernel_address} -n $kext\n";
336             }
337             system( $kextprog,
338                     "-k", $kernel,
339                     "-s", $temp_dir,
340                     "-a", 'org.openafs.filesystems.afs@' . $kernel_address,
341                     "-n", $kext );
342         } else {
343             if ($option_verbose) {
344                 print "$kextprog -c -e -r $temp_dir -k $kernel -s $temp_dir -a org.openafs.filesystems.afs\@${kernel_address} -n $kext\n";
345             }
346             system( $kextprog,
347                     "-c", "-e",
348                     "-r", $temp_dir,
349                     "-k", $kernel,
350                     "-s", $temp_dir,
351                     "-a", 'org.openafs.filesystems.afs@' . $kernel_address,
352                     "-n", $kext );
353         }
354     } else {
355         if ($kernel eq "/mach_kernel") {
356             if ($option_verbose) {
357                 print "$kextprog -k $kernel -s $temp_dir -arch $kextarch -a org.openafs.filesystems.afs\@${kernel_address} -n $kext\n";
358             }
359             system( $kextprog,
360                     "-k", $kernel,
361                     "-s", $temp_dir,
362                     "-arch", $kextarch,
363                     "-a", 'org.openafs.filesystems.afs@' . $kernel_address,
364                     "-n", $kext );
365         } else {
366             if ($option_verbose) {
367                 print "$kextprog -c -e -r $temp_dir -k $kernel -s $temp_dir -arch $kextarch -a org.openafs.filesystems.afs\@${kernel_address} -n $kext\n";
368             }
369             system( $kextprog,
370                     "-c", "-e",
371                     "-r", $temp_dir,
372                     "-k", $kernel,
373                     "-s", $temp_dir,
374                     "-arch", $kextarch,
375                     "-a", 'org.openafs.filesystems.afs@' . $kernel_address,
376                     "-n", $kext );
377         }
378     }    
379     if ( $CHILD_ERROR ) {
380         # error
381         croak "kextload failed to run: $OS_ERROR\n";
382     }
383 }
384
385
386 sub write_gdb_input_file {
387
388     my $write_dir     = shift;
389     my $filename      = shift;
390     my $backtrace_ref = shift;
391     
392     my @symbol_files = ( $write_dir . "/org.openafs.filesystems.afs.sym" );
393         
394     my $fh = IO::File->new( $write_dir . "/" . $filename, '>' )
395         or croak "Can't open gdb file $filename for writing: $OS_ERROR\n";
396
397     for my $symbol ( @symbol_files ) {
398         print $fh "add-symbol-file $symbol\n";
399     }
400     
401     print $fh "set print asm-demangle on\n";
402
403     for my $address ( @{ $backtrace_ref } ) {
404         print $fh "x/i $address\n";
405     }
406
407    $fh->close()
408         or croak "Can't close file $filename: $OS_ERROR\n";
409 }
410
411 # write out the pertinent details to a file.
412 sub write_dump_file {
413     my $filename = shift;
414     my $hash_ref = shift;
415     my $output   = shift;
416
417     my $log_dir  = dirname $filename;
418
419     if ( ! -d $log_dir ) {
420         mkdir $log_dir, 0755;
421         croak "Can't create directory $log_dir: $OS_ERROR\n" if $CHILD_ERROR;
422     }
423
424     croak "Can't write to folder $log_dir." if ( ! -w $log_dir );
425
426     my $fh = IO::File->new( $filename, '>', 0664 )
427         or croak "Can't open dump file $filename for writing: $OS_ERROR\n";
428     
429     print $fh "Panic Date:      ", $hash_ref->{ "date" }, "\n";
430     print $fh "Kernel Version:  ", $hash_ref->{ "kernel_version" }, "\n";
431     print $fh "OpenAFS Version: ", $hash_ref->{ "afs_info" }, "\n";
432     print $fh $hash_ref->{ "warning" };
433     print $fh "=============\n";
434     print $fh $output;
435     
436     $fh->close()
437         or croak "Can't close file $filename: $OS_ERROR\n";
438 }
439
440 __END__
441
442 =head1 NAME
443
444 decode-panic - decode a Mac OS panic log to show source line numbers
445
446 =head1 VERSION
447
448 This documentation refers to decode-panic version $Revision$
449
450 =head1 SYNOPSIS
451  
452    decode-panic [-i <input panic log>] [-o <output dump file>] [-k <kernel file>] [-d <kernel debug kit archive>] [-a <openafs package archive>] [-u <path to hdutil>] [-q] [-v]
453
454 =head1 OPTIONS
455
456    -i The path to the panic log that should be read
457    -o The path to where the decoded panic log should be written
458    -k The path to the kernel image corresponding to the panic
459    -d The path to a directory containing kernel debug kit dmgs
460    -a The path to an archive of OpenAFS installer dmgs
461    -u The path to the hdutil dmg utility program
462    -q Quiet mode - don't complain if there is a problem.
463    -v Verbose mode - print all commands.
464    -h print full help
465
466 =head1 DESCRIPTION
467
468 This tool parses the panic log for Mac OS X kernel panics that are caused by
469 openafs in order to produce a human-readable backtrace.
470
471 This program uses crash isolation procedure as outlined in
472 http://developer.apple.com/technotes/tn2002/tn2063.html#IsolatingCrash
473
474 Here is an example file that is fed to gdb:
475
476    add-symbol-file /tmp/afsdebugt8dGOb/org.openafs.filesystems.afs.sym
477    set print asm-demangle on
478    x/i 0x2ED1F7C0
479    x/i 0x2ED0D1A4
480
481 Panic logs can be found in /Library/Logs/panic.log in 10.4 (Tiger), 
482 /Library/Logs/PanicReporter/YYYY-MM-DD-HHMMSS.panic in 10.5 (Leopard),
483 and /Library/Logs/DiagnosticReports/Kernel_YYYY-MM-DD-HHMMSS.panic in 10.6
484 (SnowLeopard).
485
486 =head1 DEPENDENCIES
487
488 This program needs gdb and kextload; Starting in SnowLeopard, it needs kextutil.
489
490 Batch decoding requires a directory of Kernel Debug Kit DMGs, a directory of
491 OpenAFS installer DMGs, and the DMG extraction utility currently available
492 in source form at http://www.dementia.org/~shadow/dmgutil-0.1.tar.gz
493
494 =head1 BUGS AND LIMITATIONS
495
496 decode-panic clobbers the output file.
497
498 =head1 AUTHOR
499
500 Copyright 2008-2010. Jason Edgecombe <jason@rampaginggeek.com> and others.
501
502 This documentation is covered by the BSD License as written in the
503 doc/LICENSE file in the OpenAFS source tree. This program was originally
504 written by Jason Edgecombe for OpenAFS.