xref: /illumos-gate/usr/src/tools/scripts/interface_check.pl (revision ecd9ba3c681525bcbb1953941065d28b8ab9a4ec)
1#!/usr/bin/perl -w
2#
3# CDDL HEADER START
4#
5# The contents of this file are subject to the terms of the
6# Common Development and Distribution License (the "License").
7# You may not use this file except in compliance with the License.
8#
9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10# or http://www.opensolaris.org/os/licensing.
11# See the License for the specific language governing permissions
12# and limitations under the License.
13#
14# When distributing Covered Code, include this CDDL HEADER in each
15# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16# If applicable, add the following below this CDDL HEADER, with the
17# fields enclosed by brackets "[]" replaced with your own identifying
18# information: Portions Copyright [yyyy] [name of copyright owner]
19#
20# CDDL HEADER END
21#
22
23#
24# Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved.
25#
26
27#
28# Copyright 2026 Oxide Computer Company
29#
30
31#
32# Check versioning information.
33#
34# This script descends a directory hierarchy inspecting ELF shared objects for
35# version definitions.  The general theme is to verify that common versioning
36# rules have been used to build these objects.
37#
38# As always, a number of components don't follow the rules, or require
39# special handling. An exceptions file is used to specify these cases.
40#
41# By default any file that has conditions that should be reported is first
42# listed and then each condition follows.  The -o (one-line) option produces a
43# more terse output which is better for sorting/diffing with "nightly".
44#
45# Besides the default operation of checking the files within a directory
46# hierarchy, a detailed analysis of each files versions can be created with the
47# -d option.  The database created is useful for auditing the difference between
48# different builds, and for thus monitoring that versioning changes are made in
49# a compatible manner.
50
51
52# Define all global variables (required for strict)
53use vars  qw($Prog $Tmpdir);
54use vars  qw(%opt @SaveArgv $ErrFH $ObjCnt $Jobs);
55
56
57# An exception file is used to specify regular expressions to match
58# objects. These directives specify special attributes of the object.
59# The regular expressions are read from the file and compiled into the
60# regular expression variables.
61#
62# The name of each regular expression variable is of the form
63#
64#	$EXRE_xxx
65#
66# where xxx is the name of the exception in lower case. For example,
67# the regular expression variable for PLUGINS is $EXRE_plugins.
68#
69# onbld_elfmod::LoadExceptionsToEXRE() depends on this naming convention
70# to initialize the regular expression variables, and to detect invalid
71# exception names.
72#
73# If a given exception is not used in the exception file, its regular
74# expression variable will be undefined. Users of these variables must
75# test the variable with defined() prior to use:
76#
77#	defined($EXRE_plugins) && ($foo =~ $EXRE_plugins)
78#
79# ----
80#
81# The exceptions are:
82#
83# NONSTD_VERNAME
84#	Objects are expected to use standard names for versions.
85#	This directive is used to relax that requirement.
86#
87# NOVERDEF
88#	Objects that are not required to have a versioned name. Note that
89#	PLUGINS objects are implicitly NOVERDEF, so this directive is
90#	for use with non-plugin objects.
91#
92# PLUGINS
93#	Plugin objects are not required to have a versioned name, and are
94#	not required to be internally versioned.
95#
96use vars  qw($EXRE_nonstd_vername $EXRE_noverdef $EXRE_plugin);
97
98use strict;
99
100use POSIX qw(getenv);
101use Getopt::Std;
102use File::Basename;
103use IO::Handle;
104
105
106
107
108## ProcFile(BasePath, RelPath, Class, Type, Verdef, Alias)
109#
110# Investigate runtime attributes of a sharable object
111#
112# entry:
113#	BasePath - Base path from which relative paths are taken
114#	RelPath - Path of object taken relative to BasePath
115#	Class - ELFCLASS of object
116#	Type - ELF type of object
117#	Verdef - VERDEF if object defines versions, NOVERDEF otherwise
118#	Alias - Alias lines corresponding to the object, or an empty ('')
119#		string if there are no aliases.
120#
121sub ProcFile {
122	my($BasePath, $RelPath, $Class, $Type, $Verdef, $Alias) = @_;
123
124	my($File, $FullPath, %Vers, $VersCnt, %TopVer);
125	my($Val, $Ttl, $NotPlugin);
126
127	$FullPath = "$BasePath/$RelPath";
128	@_ = split /\//, $RelPath;
129	$File = $_[$#_];
130
131	$Ttl = 0;
132
133	# If this object is not a symlink, does not follow the runtime
134	# versioned name convention, and it does not reside underneath
135	# a directory identified as containing plugin objects intended
136	# for use with dlopen() only, issue a warning.
137	#
138	# Note that it can only be a symlink if the user specified
139	# a single file on the command line, because the use of
140	# 'find_elf -a' is required for a symlink to be seen.
141	$NotPlugin = !defined($EXRE_plugin) || ($RelPath !~ $EXRE_plugin);
142	if (($File !~ /\.so\./) && $NotPlugin && (! -l $FullPath)) {
143		onbld_elfmod::OutMsg($ErrFH, \$Ttl, $RelPath,
144		    "does not have a versioned name");
145	}
146
147	# If there are no versions in the file we're done.
148	if ($Verdef eq 'NOVERDEF') {
149	        # Report the lack of versioning, unless the object is
150		# a known plugin, or is explicitly exempt.
151		if ($NotPlugin &&
152		    (!defined($EXRE_noverdef) || ($RelPath !~ $EXRE_noverdef))) {
153			onbld_elfmod::OutMsg($ErrFH, \$Ttl, $RelPath,
154			    "no versions found");
155		}
156		return;
157	}
158
159	# Get a hash of the top versions in the inheritance chains.
160	%TopVer = ();
161	foreach my $Line (split(/\n/, `pvs -don $FullPath 2>&1`)) {
162		$Line =~ s/^.*-\s*(.*);/$1/;
163		$TopVer{$Line} = 1;
164	}
165
166	# Determine the name used for the base version. It should match the
167	# soname if the object has one, and the object basename otherwise.
168	#
169	# Note that elfedit writes an error to stderr if the object lacks an
170	# soname, so we direct stderr to /dev/null.
171	my $soname =
172	    `elfedit -r -osimple -e 'dyn:value dt_soname' $FullPath 2>/dev/null`;
173	if ($soname eq '') {
174		$soname = $File;
175	} else {
176		chomp $soname;
177	}
178
179	# First determine what versions exist that offer interfaces.  pvs -dos
180	# will list these.  Note that other versions may exist, ones that
181	# don't offer interfaces ... we'll get to those next.
182	%Vers = ();
183	$VersCnt = 0;
184	my %TopNumberedVers = ();
185	foreach my $Line (split(/\n/, `pvs -dos $FullPath 2>&1`)) {
186		my($Ver) = $Line;
187
188		$Ver =~ s/^.*-\t(.*): .*/$1/;		# isolate version
189
190		# See if we've already caught this version name. We only look
191		# at each version once.
192		next if ($Vers{$Ver}) ;
193
194		# Note that the non-empty version has been seen
195		$Vers{$Ver} = 1;
196		$VersCnt++;
197
198		# Identify the version type
199		my @Cat = onbld_elfmod_vertype::Category($Ver, $soname);
200
201
202		# Numbered public versions have the form
203		#
204		#	<prefix>major.minor[.micro]
205		#
206		# with 2 or three numeric values. We expect these versions to
207		# use inheritance, so there should only be one top version for
208		# each major number. It is possible, though rare, to have more
209		# than one top version if the major numbers differ.
210		#
211		# %TopNumberedVers uses the prefix and major number as the
212		# key. Each key holds a reference to an array which contains
213		# the top versions with the same prefix and major number.
214		if ($Cat[0] eq 'NUMBERED') {
215			push @{$TopNumberedVers{"$Cat[2]$Cat[3]"}}, $Ver
216			    if $TopVer{$Ver};
217			next;
218		}
219
220		# If it is a non-standard version, and there's not an
221		# exception in place for it, report an error.
222		if ($Cat[0] eq 'UNKNOWN') {
223			if (!defined($EXRE_nonstd_vername) ||
224			    ($RelPath !~ $EXRE_nonstd_vername)) {
225				onbld_elfmod::OutMsg($ErrFH, \$Ttl, $RelPath,
226				   "non-standard version name: $Ver");
227			}
228			next;
229		}
230
231		# If we are here, it is one of PLAIN, PRIVATE, or SONAME,
232		# all of which we quietly accept.
233		next;
234	}
235
236	# If this file has been scoped, but not versioned (i.e., a mapfile was
237	# used to demote symbols but no version name was applied to the
238	# global interfaces) then it's another non-standard case.
239	if ($VersCnt eq 0) {
240		onbld_elfmod::OutMsg($ErrFH, \$Ttl, $RelPath,
241		    "scoped object contains no versions");
242		return;
243	}
244
245	# If this file has multiple inheritance chains starting with the
246	# same prefix and major number, that's wrong.
247	foreach my $Ver (sort keys %TopNumberedVers) {
248		if (scalar(@{$TopNumberedVers{$Ver}}) > 1) {
249			onbld_elfmod::OutMsg($ErrFH, \$Ttl, $RelPath,
250			    "multiple $Ver inheritance chains (missing " .
251			    "inheritance?): " .
252			    join(', ', @{$TopNumberedVers{$Ver}}));
253		}
254	}
255
256
257	# Produce an interface description for the object.
258	# For each version, generate a VERSION declaration of the form:
259	#
260	#	[TOP_]VERSION  version  direct-count  total-count
261	#		symname1
262	#		symname2
263	#		...
264	#
265	# We suppress base and private versions from this output.
266	# Everything else goes in, whether it's a version we recognize
267	# or not. If an object only has base or private versions, we do
268	# not produce an interface description for that object.
269	#
270	if ($opt{i}) {
271		my $header_done = 0;
272
273		# The use of 'pvs -v' is to identify the BASE version
274		foreach my $Line (split(/\n/, `pvs -dv $FullPath 2>&1`)) {
275			# Skip base version
276			next if ($Line =~ /\[BASE\]/);
277
278			# Directly inherited versions follow the version name
279			# in a comma separated list within {} brackets. Capture
280			# that information, for use with our VERSION line.
281			my $InheritVers = ($Line =~ /(\{.*\});$/) ? "\t$1" : '';
282
283			# Extract the version name
284			$Line =~ s/^\s*([^;: ]*).*/$1/;
285
286			# Skip version if it is in the SONAME or PRIVATE
287			# categories.
288			#
289			# The above test for BASE should have caught the
290			# SONAME already, but older versions of pvs have a
291			# bug that prevents them from printing [BASE] on
292			# the base version. In order to solidify things even
293			# more, we also exclude versions that end with
294			# a '.so.*' suffix.
295			my @Cat = onbld_elfmod_vertype::Category($Line, $soname);
296			if (($Cat[0] eq 'SONAME') ||
297			    ($Cat[0] eq 'PRIVATE') ||
298			    ($Line =~ /\.so\.\d+$/)) {
299			    next;
300			}
301
302			# We want to output the symbols in sorted order, so
303			# we gather them first, and then sort the results.
304			# An array would suffice, but we have observed objects
305			# with odd inheritance chains in which the same
306			# sub-version gets inherited more than once, leading
307			# to the same symbol showing up more than once. Using
308			# a hash instead of an array thins out the duplicates.
309			my %Syms = ();
310			my $symitem = $opt{I} ? 'NEW' : 'SYMBOL';
311			my $version_cnt = 0;
312			foreach my $Sym
313			    (split(/\n/, `pvs -ds -N $Line $FullPath 2>&1`)) {
314				if ($Sym =~ /:$/) {
315					$version_cnt++;
316					# If this is an inherited sub-version,
317					# we don't need to continue unless
318					# generating output in -I mode.
319					if ($version_cnt >= 2) {
320						last if !$opt{I};
321						$symitem = 'INHERIT';
322					}
323					next;
324				}
325				$Sym =~ s/[ \t]*(.*);$/$1/;
326				$Sym =~ s/ .*$//;	# remove any data size
327				$Syms{$Sym} = $symitem;
328			}
329
330			if (!$header_done) {
331				print INTFILE "\n" if !$opt{h} && ($ObjCnt != 0);
332				$ObjCnt++;
333				print INTFILE "OBJECT\t$RelPath\n";
334				print INTFILE "CLASS\tELFCLASS$Class\n";
335				print INTFILE "TYPE\tET_$Type\n";
336				print INTFILE $Alias if ($Alias ne '');
337				$header_done = 1;
338			}
339
340			my $item = $TopVer{$Line} ? 'TOP_VERSION' : 'VERSION';
341			print INTFILE "$item\t$Line$InheritVers\n";
342
343			# Output symbols in sorted order
344			foreach my $Sym (sort keys %Syms) {
345				print INTFILE "\t$Syms{$Sym}\t$Sym\n";
346			}
347		}
348	}
349}
350
351## ProcFindElf(file)
352#
353# Open the specified file, which must be produced by "find_elf -r",
354# and process the files it describes.
355sub ProcFindElf {
356	my $file = $_[0];
357	my $line;
358	my $LineNum = 0;
359	my $prefix;
360	my @ObjList = ();
361	my %ObjToAlias = ();
362
363	open(FIND_ELF, $file) || die "$Prog: Unable to open $file";
364
365	# This script requires relative paths, created by the 'find_elf -r'
366	# option. When this is done, the first non-comment line will always
367	# be PREFIX. Obtain that line, or issue a fatal error.
368	while ($line = onbld_elfmod::GetLine(\*FIND_ELF, \$LineNum)) {
369		if ($line =~ /^PREFIX\s+(.*)$/) {
370			$prefix = $1;
371			last;
372		}
373
374		die "$file: PREFIX expected on line $LineNum\n";
375	}
376
377
378	# Process the remainder of the file.
379	while ($line = onbld_elfmod::GetLine(\*FIND_ELF, \$LineNum)) {
380		if ($line =~ /^OBJECT\s/i) {
381			my ($item, $class, $type, $verdef, $obj) =
382			    split(/\s+/, $line, 5);
383
384			# We are only interested in sharable objects. We may
385			# see other file types if processing a list of
386			# objects supplied via the -f option.
387			push @ObjList, $line if $type eq 'DYN';
388			next;
389		}
390
391		if ($line =~ /^ALIAS\s/i) {
392			my ($item, $obj, $alias) = split(/\s+/, $line, 3);
393			my $str = "ALIAS\t$alias\n";
394
395			if (defined($ObjToAlias{$obj})) {
396				$ObjToAlias{$obj} .= $str;
397			} else {
398				$ObjToAlias{$obj} = $str;
399			}
400		}
401	}
402
403	close FIND_ELF;
404
405	if ($Jobs > 1) {
406		ProcObjListParallel($prefix, \@ObjList, \%ObjToAlias);
407	} else {
408		ProcObjList($prefix, \@ObjList, \%ObjToAlias, 0,
409		    scalar(@ObjList));
410	}
411}
412
413sub ProcObjList {
414	my ($prefix, $objs, $alias, $lo, $hi) = @_;
415
416	for (my $i = $lo; $i < $hi; $i++) {
417		my ($item, $class, $type, $verdef, $obj) =
418		    split(/\s+/, $objs->[$i], 5);
419		my $a = defined($alias->{$obj}) ? $alias->{$obj} : '';
420
421		ProcFile($prefix, $obj, $class, $type, $verdef, $a);
422	}
423}
424
425sub AppendFile {
426	my ($file, $fh) = @_;
427	my $line;
428
429	open(APPEND, $file) || return;
430	print $fh $line while ($line = <APPEND>);
431	close APPEND;
432}
433
434sub ProcObjListParallel {
435	my ($prefix, $objs, $alias) = @_;
436	my $cnt = scalar(@$objs);
437	my $jobs = ($Jobs > $cnt) ? $cnt : $Jobs;
438	my (@Pids, @Errs, @Ints);
439	my $failed = 0;
440
441	my $workdir = "$Tmpdir/$Prog.$$";
442	mkdir($workdir, 0700) || die "$Prog: mkdir failed: $workdir: $!";
443
444	$ErrFH->flush();
445	(\*INTFILE)->flush() if $opt{i};
446	(\*STDOUT)->flush();
447
448	# The child reaping below relies on the default disposition of
449	# SIGCHLD.
450	local $SIG{CHLD} = 'DEFAULT';
451
452	# Each child is given a contiguous slice of the object list. A slice
453	# starts where the previous one ended, and since $jobs has been
454	# clamped to the number of objects no slice is empty.
455	my ($lo, $hi) = (0, 0);
456	for (my $i = 0; $i < $jobs; $lo = $hi, $i++) {
457		$hi = int(($cnt * ($i + 1)) / $jobs);
458
459		my $efile = "$workdir/err.$i";
460		my $ifile = "$workdir/intf.$i";
461		my $pid = fork();
462
463		if (!defined($pid)) {
464			# Fall through so that the children despatched so
465			# far are still reaped and their output collected
466			# before the failure is reported.
467			warn "$Prog: fork failed: $!\n";
468			$failed = 1;
469			last;
470		}
471
472		if ($pid == 0) {
473			# Child - redirect the error output, and any
474			# interface description output, to temporary files
475			# and process our slice of the object array.
476			open(CHLDERR, ">$efile") ||
477			    die "$Prog: open failed: $efile: $!";
478			$ErrFH = \*CHLDERR;
479			if ($opt{i}) {
480				open(INTFILE, ">$ifile") ||
481				    die "$Prog: open failed: $ifile: $!";
482				$ObjCnt = 0;
483			}
484
485			ProcObjList($prefix, $objs, $alias, $lo, $hi);
486
487			close(CHLDERR) ||
488			    die "$Prog: close failed: $efile: $!";
489			if ($opt{i}) {
490				close(INTFILE) ||
491				    die "$Prog: close failed: $ifile: $!";
492			}
493			exit 0;
494		}
495
496		push @Pids, $pid;
497		push @Errs, $efile;
498		push @Ints, $ifile;
499	}
500
501	for (my $i = 0; $i <= $#Pids; $i++) {
502		next if waitpid($Pids[$i], 0) != -1 && $? == 0;
503
504		warn "$Prog: child " . ($i + 1) . "/" . scalar(@Pids) .
505		    " failed: status $?\n";
506		$failed = 1;
507	}
508
509	for (my $i = 0; $i <= $#Errs; $i++) {
510		AppendFile($Errs[$i], $ErrFH);
511		unlink $Errs[$i];
512
513		next if !$opt{i};
514
515		if (-s $Ints[$i]) {
516			print INTFILE "\n" if !$opt{h} && ($ObjCnt != 0);
517			AppendFile($Ints[$i], \*INTFILE);
518			$ObjCnt++;
519		}
520		unlink $Ints[$i];
521	}
522
523	rmdir $workdir;
524
525	die "$Prog: parallel object processing failed\n" if $failed;
526	die "$Prog: only $hi of $cnt objects were processed\n" if $hi != $cnt;
527}
528
529
530# -----------------------------------------------------------------------------
531
532# Establish a program name for any error diagnostics.
533chomp($Prog = `basename $0`);
534
535# Check that we have arguments.
536@SaveArgv = @ARGV;
537if ((getopts('c:E:e:f:hIi:j:ow:', \%opt) == 0) ||
538    (!$opt{f} && ($#ARGV == -1))) {
539	print "usage: $Prog [-hIo] [-c vtype_mod] [-E errfile] [-e exfile]\n";
540	print "\t\t[-f listfile] [-i intffile] [-j jobs] [-w outdir]\n";
541	print "\t\tfile | dir, ...\n";
542	print "\n";
543	print "\t[-c vtype_mod]\tsupply alternative version category module\n";
544	print "\t[-E errfile]\tdirect error output to file\n";
545	print "\t[-e exfile]\texceptions file\n";
546	print "\t[-f listfile]\tuse file list produced by find_elf -r\n";
547	print "\t[-h]\t\tdo not produce a CDDL/Copyright header comment\n";
548	print "\t[-I]\t\tExpand inheritance in -i output (debugging)\n";
549	print "\t[-i intffile]\tcreate interface description output file\n";
550	print "\t[-j jobs]\tprocess objects in parallel with this many jobs\n";
551	print "\t[-o]\t\tproduce one-liner output (prefixed with pathname)\n";
552	print "\t[-w outdir]\tinterpret all files relative to given directory\n";
553	exit 1;
554}
555
556$Jobs = 1;
557if (defined($opt{j})) {
558	die "$Prog: -j requires a positive integer\n"
559	    if $opt{j} !~ /^\d+$/ || $opt{j} == 0;
560	$Jobs = $opt{j};
561}
562
563$Tmpdir = "/tmp" if (!($Tmpdir = $ENV{TMPDIR}) || (! -d $Tmpdir));
564
565# We depend on the onbld_elfmod and onbld_elfmod_vertype perl modules.
566# Both modules are maintained in the same directory as this script,
567# and are installed in ../lib/perl. Use the local one if present,
568# and the installed one otherwise.
569#
570# The caller is allowed to supply an alternative implementation for
571# onbld_elfmod_vertype via the -c option. In this case, the alternative
572# implementation is expected to provide the same interface as the standard
573# copy, and is loaded instead.
574#
575my $moddir = my $vermoddir = dirname($0);
576$moddir = "$moddir/../lib/perl" if ! -f "$moddir/onbld_elfmod.pm";
577require "$moddir/onbld_elfmod.pm";
578if ($opt{c}) {
579	require "$opt{c}";
580} else {
581	$vermoddir = "$vermoddir/../lib/perl"
582	    if ! -f "$vermoddir/onbld_elfmod_vertype.pm";
583	require "$vermoddir/onbld_elfmod_vertype.pm";
584}
585
586# If -w, change working directory to given location
587!$opt{w} || chdir($opt{w}) || die "$Prog: can't cd to $opt{w}";
588
589
590# Error messages go to stdout unless -E is specified. $ErrFH is a
591# file handle reference that points at the file handle where error messages
592# are sent.
593if ($opt{E}) {
594	open(ERROR, ">$opt{E}") || die "$Prog: open failed: $opt{E}";
595	$ErrFH = \*ERROR;
596} else {
597	$ErrFH = \*STDOUT;
598}
599
600# Locate and process the exceptions file
601onbld_elfmod::LoadExceptionsToEXRE('interface_check');
602
603# If creating an interface description output file, prepare it for use
604if ($opt{i}) {
605	open (INTFILE, ">$opt{i}") ||
606	    die "$Prog: Unable to create file: $opt{i}";
607
608	# Generate the output header
609	onbld_elfmod::Header(\*INTFILE, $0, \@SaveArgv) if !$opt{h};;
610}
611
612# Number of OBJECTs output to INTFILE
613$ObjCnt = 0;
614
615# If we were passed a file previously produced by 'find_elf -r', use it.
616ProcFindElf($opt{f}) if $opt{f};
617
618# Process each argument: Run find_elf to find the files given by
619# $Arg. If the argument is a regular file (not a directory) then disable
620# find_elf's alias checking so that the file is processed whether or not
621# it is a symlink.
622foreach my $Arg (@ARGV) {
623	my $flag_a = (-d $Arg) ? '' : '-a';
624	ProcFindElf("find_elf -frs $flag_a $Arg|");
625}
626
627# Close any working output files.
628close INTFILE if $opt{i};
629close ERROR if $opt{E};
630
631exit 0;
632