1 #!/usr/perl5/bin/perl -w
5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License, Version 1.0 only
7 # (the "License"). You may not use this file except in compliance
10 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
11 # or http://www.opensolaris.org/os/licensing.
12 # See the License for the specific language governing permissions
13 # and limitations under the License.
15 # When distributing Covered Code, include this CDDL HEADER in each
16 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
17 # If applicable, add the following below this CDDL HEADER, with the
18 # fields enclosed by brackets "[]" replaced with your own identifying
19 # information: Portions Copyright [yyyy] [name of copyright owner]
24 # ident "%Z%%M% %I% %E% SMI"
26 # Copyright 2004 Sun Microsystems, Inc. All rights reserved.
27 # Use is subject to license terms.
31 # This is the top level script for performing the appcert checks. It
32 # reads the command line options, determines list of binaries to check,
33 # and then calls symprof (the raw symbol profiler), symcheck (that
34 # checks for unstable behavior), and symreport (that constructs and
35 # outputs a rollup report)
42 use POSIX
qw(locale_h);
43 use Sun
::Solaris
::Utils
qw(textdomain gettext);
47 use lib
qw(/usr/lib/abi/appcert);
50 setlocale
(LC_ALL
, "");
51 textdomain
(TEXT_DOMAIN
);
56 $do_not_follow_symlinks
58 $append_solaris_dirs_to_ld_path
62 my $caught_signal = 0;
63 my $record_binary_call_count = 0;
65 # The directory where the appcert specific scripts and data reside:
66 $appcert_lib_dir = "/usr/lib/abi/appcert";
68 set_clean_up_exit_routine
(\
&clean_up_exit
);
70 signals
('on', \
&interrupted
);
74 @item_list = @ARGV; # List of directories and/or objects to check.
79 find_binaries
(); # Records all of the binary objects to check.
81 supplement_ld_library_path
();
83 export_vars_to_environment
(); # Exports info for our child scripts to use.
85 run_profiler
(); # Run the script symprof.
87 run_checker
(); # Run script symcheck.
89 run_report_generator
(); # Run the script symreport.
91 my $rc = overall_result_code
();
99 # This subroutine calls getopts() and sets up variables reflecting how
106 getopts
('?hnLBSw:f:', \
%opt) || (show_usage
() && exiter
(2));
108 if (exists($opt{'?'}) || exists($opt{'h'})) {
113 if (exists($opt{'f'})) {
114 $file_list = $opt{'f'};
119 if (exists($opt{'w'})) {
120 $working_dir = $opt{'w'};
124 if ($working_dir =~ /'/) {
126 # This character will ultimately cause problems with
127 # system() and pipelines so we exit now.
129 exiter
(sprintf(gettext
(
130 "directory contains the single-quote character ': %s\n"),
134 if (defined($opt{'B'})) {
140 if (defined($opt{'n'})) {
141 $do_not_follow_symlinks = 1;
143 $do_not_follow_symlinks = 0;
146 if (defined($opt{'L'})) {
152 if (defined($opt{'S'})) {
153 $append_solaris_dirs_to_ld_path = 1;
155 $append_solaris_dirs_to_ld_path = 0;
160 # Performs an initial check to see if the user supplied anything at all
161 # to check. Also reads in the file list if the user supplied one via -f <file>
165 # Add the items if the -f flag was used.
168 my $list_fh = do { local *FH
; *FH
};
169 if (-f
$file_list && open($list_fh, "<$file_list")) {
172 push(@item_list, $file);
176 exiter
(nofile
($file_list, $!));
180 return if (@item_list);
182 emsg
("$command_name: " . gettext
(
183 "at least one file or directory to check must be specified.") .
191 # This subroutine sets up the working directory, the default something
192 # like: /tmp/appcert.<PID>
197 # working_dir has been set in get_options().
198 if (! -d
$working_dir) {
199 if (! mkpath
($working_dir) || ! -d
$working_dir) {
200 exiter
(nocreatedir
($working_dir, $!));
203 if (! dir_is_empty
($working_dir)) {
204 # create a subdir of it for our use.
205 $working_dir = create_tmp_dir
($working_dir);
209 # Default case: will create, e.g., /tmp/appcert.12345
210 $working_dir = create_tmp_dir
();
213 if (! -d
$working_dir) {
214 # We have no working directory.
215 exiter
(nocreatedir
($working_dir));
219 # Create a subdirectory of working_dir that will contain all of
220 # the object subdirs.
222 my $dir = "$working_dir/$object_dir";
223 if (! mkpath
($dir) || ! -d
$dir) {
224 exiter
(nocreatedir
($dir, $!));
227 # Make a tmp subdirectory for small temporary work. It is
228 # preferred to have it on tmpfs (especially not NFS) for
229 # performance reasons.
231 $tmp_dir = "/tmp/${command_name}_tmp.$$";
233 exiter
(nocreatedir
("$tmp_dir", $!));
235 if (! mkpath
($tmp_dir, 0, 0700) || ! -d
$tmp_dir) {
236 emsg
("%s", nocreatedir
($tmp_dir, $!));
237 # fall back to our output dir (which could have slow access)
238 $tmp_dir = "$working_dir/tmp";
239 if (! mkpath
($tmp_dir)) {
240 exiter
(nocreatedir
($tmp_dir, $!));
245 exiter
(nocreatedir
($tmp_dir, $!));
250 # Top level function to find all the binaries to be checked. Calls
251 # record_binary() to do the actual deciding and recording.
253 # The array @item_list contains all the items to find.
259 my $skipped_file = "$working_dir/Skipped";
260 my $skipped_fh = do { local *FH
; *FH
};
261 open($skipped_fh, ">$skipped_file") ||
262 exiter
(nofile
($skipped_file, $!));
266 my ($item, $args, $file);
268 "finding executables and shared libraries to check") . " ...\n");
271 $args .= '-follow ' unless ($do_not_follow_symlinks);
272 $args .= '-type f -print';
274 my $quote_fmt = gettext
(
275 "skipping: item contains the single-quote character ': %s\n");
277 foreach $item (@item_list) {
279 emsg
(gettext
("skipping: %s: %s\n"), $item, $!);
280 print $skipped_fh "$item: no_exist\n";
283 } elsif ($item =~ /'/) {
284 emsg
($quote_fmt, $item);
285 print $skipped_fh "$item: item_has_bad_char\n";
289 # note that $item does not contain a single-quote.
290 my $find_fh = do { local *FH
; *FH
};
291 open($find_fh, "$cmd_find '$item' $args|") ||
292 exiter
(norunprog
("$cmd_find '$item' $args", $!));
297 # We are free to remove leading "./". This will
298 # minimize directory names we create that would
303 next if ($file eq '');
305 record_binary
($file, $skipped_fh);
310 if ($binary_count == 0) {
311 exiter
("$command_name: " . gettext
(
312 "no checkable binary objects were found."), 3);
315 if ($skipped_count == 0) {
316 print $skipped_fh "# NO_FILES_WERE_SKIPPED\n";
322 # This subroutine will determine if a binary is checkable.
324 # If so, it will reserve a directory for its output in the $working_dir
325 # location, and store the output of a number of commands there.
329 my ($file, $skipped_fh) = @_;
331 if ((++$record_binary_call_count % 500) == 0) {
333 # This indicates are being called many times for a large
334 # product. Clear out our caches.
340 # Check if the object exists and is regular file. Note that
341 # this test also passes a symlink as long as that symlink
342 # ultimately refers to a regular file.
345 emsg
(gettext
("skipping: not a file: %s\n"), $file);
346 print $skipped_fh "$file: not_a_file\n";
351 # Check if it is readable:
353 emsg
(gettext
("skipping: cannot read: %s\n"), $file);
354 print $skipped_fh "$file: unreadable\n";
360 # Since the filename will be used as operands passed to utility
361 # commands via the shell, we exclude at the outset certain meta
362 # characters in the filenames.
364 my $quote_fmt = gettext
(
365 "skipping: filename contains the single-quote character: ': %s\n");
367 emsg
($quote_fmt, $file);
368 print $skipped_fh "$file: filename_has_bad_char\n";
373 my $newline_fmt = gettext
(
374 "skipping: filename contains the newline character: \\n: %s\n");
376 emsg
($newline_fmt, $file);
377 print $skipped_fh "$file: filename_has_bad_char\n";
382 my $pipe_fmt = gettext
(
383 "skipping: filename contains the pipe character: \|: %s\n");
385 emsg
($pipe_fmt, $file);
386 print $skipped_fh "$file: filename_has_bad_char\n";
393 # Run the file(1) command on it.
396 # note that $file does not contain a single-quote.
397 $file_output = `$cmd_file '$file' 2>/dev/null`;
400 if ($file_output =~ /script$/) {
401 $file_output =~ s/:\s+/: /;
402 $file_output =~ s/: /: script /;
403 print $skipped_fh "$file_output";
406 # again now without the c_locale() setting:
407 # note that $file does not contain a single-quote.
409 $file_output = `$cmd_file '$file' 2>/dev/null`;
410 $file_output =~ s/:\s+/: /;
411 emsg
(gettext
("skipping: %s"), $file_output);
416 # create ELF and a.out matching regex:
418 'ELF.*executable.*dynamically' . '|' .
419 'ELF.*dynamic lib' . '|' .
420 'ELF.*executable.*statically' . '|' .
421 'Sun demand paged SPARC.*dynamically linked' . '|' .
422 'Sun demand paged SPARC executable' . '|' .
423 'pure SPARC executable' . '|' .
424 'impure SPARC executable';
427 # Note that we let the "statically linked" binaries through
428 # here, but will catch them later in the profiler and checker.
431 if ($file_output !~ /$object_match/io) {
432 # it is not an ELF object file and so does not interest us.
436 my $exec_fmt = gettext
(
437 "skipping: must have exec permission to be checked: %s\n");
440 # It interests us, but the execute bit not set. Shared
441 # objects will be let through here since ldd will still
442 # work on them (since it uses lddstub). Otherwise, we
445 if (! is_shared_object
($file)) {
446 # warn the user exec bit should be set:
447 emsg
($exec_fmt, $file);
448 print $skipped_fh "$file: no_exec_permission\n";
455 # Rather than let ldd fail later on in symprof, we check the
456 # arch here to make sure it matches $uname_p. If it does not
457 # match, we anticipate a 64-bit application and so we
458 # immediately test how ldd will handle it (kernel might be
461 my ($arch, $type, $wordsize, $endian, $e_machine) = bin_type
($file);
463 if ($arch !~ /^${uname_p}$/io) {
464 my ($ldd_output, $ldd_output2);
467 # Now run ldd on it to see how things would go. If it
468 # fails we must skip it.
471 # note that $file does not contain single-quote
472 $ldd_output = `$cmd_ldd '$file' 2>&1 1>/dev/null`;
475 # note that $file does not contain a single-quote
476 $ldd_output2 = `$cmd_ldd '$file' 2>&1 1>/dev/null`;
477 $ldd_output =~ s/\n.*$//;
478 $ldd_output2 =~ s/\n.*$//;
479 if ($ldd_output !~ /wrong class/) {
480 $ldd_output = "$file: " . sprintf(
481 gettext
("ldd failed for arch: %s"), $arch);
482 $ldd_output2 = $ldd_output;
484 $ldd_output .= " ($arch)";
485 $ldd_output2 .= " ($arch)";
487 $ldd_output =~ s/:\s+/: /;
488 $ldd_output2 =~ s/:\s+/: /;
489 emsg
(gettext
("skipping: %s\n"), $ldd_output2);
490 $ldd_output =~ s/: /: ldd_failed /;
491 print $skipped_fh "$ldd_output\n";
497 # From this point on, object is one we decided to check.
499 # Create the directory name for this object:
500 my $dirname = object_to_dir_name
($file);
501 my $dirpath = "$working_dir/$dirname";
502 my $early_fmt = gettext
(
503 "skipping: %s referenced earlier on the command line\n");
506 # Directory already exists. We assume this means the
507 # user listed it twice (possibly indirectly via "find").
509 emsg
($early_fmt, $file);
513 if (! mkdir($dirpath, 0777)) {
514 exiter
(nocreatedir
($dirpath, $!));
519 # Record binary object's location:
520 my $path_fh = do { local *FH
; *FH
};
521 open($path_fh, ">$dirpath/info.path") ||
522 exiter
(nofile
("$dirpath/info.path", $!));
523 print $path_fh $file, "\n";
527 # Record /usr/bin/file output. Note that the programmatical way
528 # to access this info is through the command cmd_output_file().
530 my $file_fh = do { local *FH
; *FH
};
531 open($file_fh, ">$dirpath/info.file") ||
532 exiter
(nofile
("$dirpath/info.file", $!));
533 print $file_fh $file_output;
537 # Record dump -Lv output. Note that the programmatical way to
538 # access this info is through the command cmd_output_dump().
540 my $dump_fh = do { local *FH
; *FH
};
541 open($dump_fh, ">$dirpath/info.dump") ||
542 exiter
(nofile
("$dirpath/info.dump", $!));
546 # note that $file does not contain a single-quote
547 $dump_output = `$cmd_dump -Lv '$file' 2>&1`;
549 print $dump_fh $dump_output;
553 # Record arch and etc binary type.
555 my $arch_fh = do { local *FH
; *FH
};
556 open($arch_fh, ">$dirpath/info.arch") ||
557 exiter
(nofile
("$dirpath/info.arch", $!));
559 if ($arch eq 'unknown') {
560 my $tmp = $file_output;
562 emsg
(gettext
("warning: cannot determine arch: %s\n"), $tmp);
565 print $arch_fh "ARCH: $arch\n";
566 print $arch_fh "TYPE: $type\n";
567 print $arch_fh "WORDSIZE: $wordsize\n";
568 print $arch_fh "BYTEORDER: $endian\n";
569 print $arch_fh "E_MACHINE: $e_machine\n";
572 # Record the file -> directory name mapping in the index file.
573 my $index_file = "$working_dir/Index";
574 my $index_fh = do { local *FH
; *FH
};
575 open($index_fh, ">>$index_file") ||
576 exiter
(nofile
($index_file, $!));
577 print $index_fh "$file => $dirname\n";
584 # Prints the usage statement to standard out.
589 "usage: appcert [ -nBLS ] [ -f file ] [ -w dir ] { obj | dir } ...\n" .
590 " Examine binary object files for use of private Solaris\n" .
591 " interfaces, unstable use of static linking, and other\n" .
592 " unstable practices.\n")
597 # Examines the set of binaries to be checked and notes which ones are
598 # shared libraries. Constructs a LD_LIBRARY_PATH that would find ALL of
599 # these shared objects. The new directories are placed at the END of the
600 # current LD_LIBRARY_PATH (if any).
602 sub supplement_ld_library_path
604 my (@orig, @add_product, @add_solaris, %ldpath);
606 # First, note the current LD_LIBRARY_PATH parts:
609 if (defined($ENV{'LD_LIBRARY_PATH'})) {
610 foreach $dirname (split(/:/, $ENV{'LD_LIBRARY_PATH'})) {
611 if (! exists($ldpath{$dirname})) {
612 push(@orig, $dirname);
613 $ldpath{$dirname} = 1;
618 # Next, search for ELF shared objects.
621 if ($modify_ld_path) {
622 while (defined($dir = next_dir_name
())) {
623 $path = dir_name_to_path
($dir);
625 $dirname = dirname
($path);
626 next if (exists($ldpath{$dirname}));
629 # A colon ":" in directory name is cannot be
630 # accepted because that is the LD_LIBRARY_PATH
633 next if ($dirname =~ /:/);
635 if (is_shared_object
($path)) {
636 if (! exists($ldpath{$dirname})) {
637 push(@add_product, $dirname);
638 $ldpath{$dirname} = 1;
644 if ($append_solaris_dirs_to_ld_path) {
645 foreach $dirname (split(/:/, $solaris_library_ld_path)) {
646 if (! exists($ldpath{$dirname})) {
647 push(@add_solaris, $dirname);
648 $ldpath{$dirname} = 1;
653 # modify the LD_LIBRARY_PATH:
654 if (@add_product || @add_solaris) {
655 $ENV{'LD_LIBRARY_PATH'} =
656 join(':', (@orig, @add_product, @add_solaris));
662 "Shared libraries were found in the application and the\n" .
663 "following directories are appended to LD_LIBRARY_PATH:\n"
666 foreach $dir (@add_product) {
667 $dir = "./$dir" unless ($dir =~ m
,^/,);
675 "These Solaris library directories are being appended\n" .
676 "to LD_LIBRARY_PATH:\n") . "\n");
678 foreach $dir (@add_solaris) {
686 # Everything is correctly exported by now, and so we just run "symprof".
687 # It is run in batches of $block_size binaries to minimize the effect of
688 # memory usage caused by huge binaries in the product to be checked.
696 # record old values of the blocks (if any)
697 my $env_min = $ENV{'AC_BLOCK_MIN'};
698 my $env_max = $ENV{'AC_BLOCK_MAX'};
700 while ($i < $binary_count) { # do each block
701 # export our symprof values of the block limits
702 $ENV{'AC_BLOCK_MIN'} = $i;
703 $ENV{'AC_BLOCK_MAX'} = $i + $block_size;
710 # restore old values of the blocks (if any)
711 if (defined($env_min)) {
712 $ENV{'AC_BLOCK_MIN'} = $env_min;
714 delete $ENV{'AC_BLOCK_MIN'};
716 if (defined($env_max)) {
717 $ENV{'AC_BLOCK_MAX'} = $env_max;
719 delete $ENV{'AC_BLOCK_MAX'};
724 # Sub that actually runs "symprof".
728 system("$appcert_lib_dir/symprof");
730 emsg
("%s", utilityfailed
("symprof"));
736 # Sub to run "symcheck".
740 system("$appcert_lib_dir/symcheck");
742 emsg
("%s", utilityfailed
("symcheck"));
748 # Sub to run "symreport".
750 sub run_report_generator
752 system("$appcert_lib_dir/symreport");
754 emsg
("%s", utilityfailed
("symreport"));
760 # General routine to be called if one of our utility programs (symprof,
761 # symcheck, symreport) failed (that is, return != 0). returns the
762 # formatted error message string to pass to the user.
768 $fmt = "\n *** " . gettext
("utility program failed: %s\n");
769 return sprintf($fmt, $prog);
773 # Does the cleanup and then exits with return code $rc. The utility
774 # subroutine exiter() will call this subroutine. No general cleanup is
775 # performed if exiting with error ($rc > 0) so that the user can examine
776 # at the output files, etc.
792 # General cleanup routine.
796 if (-d
$tmp_dir && ($tmp_dir !~ m
,^/+$,)) {
802 # Routine that is called when an error has occurred. It indicates to
803 # user where the working and/or temporary directory is and that they are
811 if (defined($working_dir) && -d
$working_dir) {
812 push(@dirlist, $working_dir);
814 if (defined($tmp_dir) && -d
$tmp_dir) {
815 push(@dirlist, $tmp_dir);
818 return if (! @dirlist);
821 "Note that the temporary working directories still exist:") .
825 # show the user explicitly which directories remains:
826 foreach $dir (@dirlist) {
827 system($cmd_ls, '-ld', $dir);
834 # Signal handler for interruptions (E.g. Ctrl-C SIGINT).
838 $SIG{$_[0]} = 'IGNORE';
840 exit 1 if ($caught_signal);
844 emsg
("\n** " . gettext
("interrupted") . " **\n");