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