############################################################################## ## ## Yard.pm combining ## MAKE_ROOT_FS, CHECK_ROOT_FS, and YARD_UTILS.PL by Tom Fawcett ## Copyright (C) 1996,1997,1998 Tom Fawcett (fawcett@croftj.net) ## Copyright (C) 2000 Modifications by the gBootRoot Team ## ## This program is free software; you may redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ## ##### ## ## This is a heavily modified version of several scripts from the Yard ## Suite (v2.0) by Tom Fawcett. The modifications allow gBootRoot to use ## Yard as a Method. ## ############################################################################## package Yard; use vars qw(@ISA @EXPORT %EXPORT_TAGS); use Exporter; @ISA = qw(Exporter); @EXPORT = qw(); use strict; use File::Basename; use File::Path; use FileHandle; use Cwd; # I am not even sure if this is being used here now use English; # I think this can be ditched for portability #use lib "@config_dest@", "@lib_dest@"; #use yardconfig;# this exports these things $scripts_dest # $lib_dest $config_dest %_path use File::Find; # used by check_root_fs # YARDCONFIG.PM ########################################## ########################################## #$scripts_dest = "@scripts_dest@"; #$lib_dest = "@lib_dest@"; #$config_dest = "@config_dest@"; #unshift(@::INC, $config_dest); # Ironically this is only used once for objcopy. #%_path =( 'perl' => '@PERL@', # 'ldd' => '@LDD@', # 'ldconfig' => '@LDCONFIG@', # 'chroot' => '@CHROOT@', # 'sync' => '@SYNC@', # 'mount' => '@MOUNT@', # 'umount' => '@UMOUNT@', # 'rm' => '@RM@', # 'dd' => '@DD@', # 'mke2fs' => '@MKE2FS@', # 'rdev' => '@RDEV@', # 'gzip' => '@GZIP@', # 'uname' => '@UNAME@', # 'objcopy' => '@OBJCOPY@' # ); ############################################ ############################################ # BEGIN { require "yard_utils.pl"; } # Supplied by gBootroot #require "Config.pl"; STDOUT->autoflush(1); start_logging_output(); info 0, "make_root_fs @yard_version@\n"; info 1, "(running under Perl $PERL_VERSION)\n"; my($objcopy) = $_path{'objcopy'}; # Define objcopy path if executable exists my($Warnings) = 0; sub warning { info 0, "Warning: ", @_; $Warnings++; } ############################################################################## ##### Check some basic things before starting. ##### There's probably a more graceful way to maintain and check ##### a set of user options (via a Perl module), but I'm too lazy ##### to track it down. ############################################################################## if ($REAL_USER_ID != 0) { error "This script must be run as root\n"; } if (!defined($::device) and !defined($::mount_point)) { error "Nothing defined in CFG package. You probably just copied\n", "an old Config.pl file.\n"; } # Check mount point if (-d $::mount_point and -w _) { info 1, "Using $::mount_point as mount point for $::device\n"; } else { error "Mount point $::mount_point must be a directory and\n", "must be write-enabled.\n"; } # Check for sane device choice before we start using it. check_device(); # Make sure $::device isn't already mounted and $::mount_point is free load_mount_info(); if (defined($mounted{$::device})) { if ($mounted{$::device} eq $::mount_point) { info 1, "Device $::device is already mounted on $::mount_point\n"; info 1, "Unmounting it automatically.\n"; sys("umount $::mount_point"); } else { error "$::device is already mounted elsewhere (on $mounted{$::device})\n", "Unmount it first.\n"; } } elsif (defined($mounted{$::mount_point})) { error "Some other device is already mounted on $::mount_point\n"; } # Have to test this every time so we can work around. test_glob(); ##### Determine release of $::kernel for modules. ##### Set RELEASE environment variable for use in contents. if (defined($::kernel_version)) { # Check to see if it agrees my($version_guess) = kernel_version($::kernel); if ($version_guess ne $::kernel_version) { info 0, "You declared kernel ($::kernel) to be version $::kernel_version,\n", "\teven though a probe says $version_guess.", "\tI'll assume you're right.\n"; } $ENV{'RELEASE'} = $::kernel_version; } elsif (defined($ENV{'RELEASE'} = kernel_version($::kernel))) { info 0, "Version probe of $::kernel returns: $ENV{'RELEASE'}\n"; } else { warning "Can't determine kernel version of $::kernel\n"; my($release) = `uname -r`; if ($release) { chomp($release); info 0, "Will use version of current running kernel ($release)\n", "Make sure this is OK\n"; $ENV{'RELEASE'} = $release; } else { error "And can't determine running kernel's version either!\n"; } } warn_about_module_dependencies($ENV{'RELEASE'}); if ($::disk_set !~ /^(single|double|base\+extra)$/) { error "Config variable disk_set is set to \"$::disk_set\"\n", "which is not a valid value.\n"; } ############################################################################## ##### READ IN CONTENTS FILE ##### ############################################################################## my($contents_file) = resolve_file($::contents_file); info 0, "\n\nPASS 1: Reading $::contents_file"; info 0, " ($contents_file)" if $contents_file ne $::contents_file; info 0, "\n"; my(%Included); my(%replaced_by); my(%links_to); my(%is_module); open(CONTENTS, "<$contents_file") or error "$contents_file: $!"; my($cf_line) = 0; my($line); LINE: while (defined($line = )) { my(@files); $cf_line++; chomp $line; $line =~ s/[\#%].*$//; # Kill comments next if $line =~ /^\s*$/; # Ignore blank/empty line $line =~ s/^\s+//; # Delete leading/trailing whitespace $line =~ s/\s+$//; # if ($line =~ /\$RELEASE/) { # cf_warn($line, "Make sure \$RELEASE ($ENV{'RELEASE'}) is correct " . # "for $::kernel"); # } if ($line =~ /->/) { ##### EXPLICIT LINK if ($line =~ /[\*\?\[]/) { cf_warn($line, "Can't use wildcards in link specification!"); next LINE; } my($file, $link) = $line =~ /^(\S+)\s*->\s*(\S+)\s*$/; if (!defined($link)) { cf_warn($line, "Can't parse this link"); next LINE; } ##### The '->' supersedes file structure on the disk, so don't ##### call include_file until pass two after all explicit links ##### have been seen. my($abs_file) = find_file_in_path($file); $Included{$abs_file} = 1; #### Have to be careful here. Record the rel link for use #### in setting up the root fs, but use the abs_link in @files #### so next loop gets any actual files. my($abs_link) = make_link_absolute($abs_file, $link); my($rel_link) = make_link_relative($abs_file, $link); $links_to{$abs_file} = $rel_link; info 1, "$line links $abs_file to $rel_link\n"; @files = ($abs_link); } elsif ($line =~ /<=/) { ##### REPLACEMENT SPEC cf_die($line, "Can't use wildcard in replacement specification") if $line =~ /[\*\?\[]/; my($file, $replacement) = $line =~ /^(\S+)\s*<=\s*(\S+)\s*$/; if (!defined($replacement)) { cf_warn($line, "Can't parse this replacement spec"); next LINE; } else { must_be_abs($file); (-d $file) and cf_warn($line, "left-hand side can't be directory"); my($abs_replacement) = find_file_in_path($replacement); if (!(defined($abs_replacement) and -e $abs_replacement)) { cf_warn($line, "Can't find $replacement"); } elsif ($replacement =~ m|^/dev/(?!null)|) { # Allow /dev/null but no other devices cf_warn($line, "Can't replace a file with a device"); } else { $replaced_by{$file} = $abs_replacement; $Included{$file} = 1; } next LINE; } # End of replacement spec } elsif ($line =~ /(<-|=>)/) { cf_warn($line, "Not a valid arrow."); next LINE; } else { @files = (); my($expr); for $expr (split(' ', $line)) { my(@globbed) = yard_glob($expr); if ($#globbed == -1) { cf_warn($line, "Warning: No files matched $expr"); } elsif (!($#globbed == 0 and $globbed[0] eq $expr)) { info 1, "Expanding $expr to @globbed\n"; } push(@files, @globbed); } } my($file); FILE: foreach $file (@files) { if ($file =~ m|^/|) { ##### Absolute filename if (-l $file and readlink($file) =~ m|^/proc/|) { info 1, "Recording proc link $file -> ", readlink($file), "\n"; $Included{$file} = 1; $links_to{$file} = readlink($file); } elsif (-e $file) { $Included{$file} = 1; } elsif ($file =~ m|^$::oldroot/(.*)$|o and -e "/$1") { ### Don't complain about links to files that will be mounted ### under $oldroot, the hard disk root mount point. next FILE; } else { cf_warn($line, "Absolute filename $file doesn't exist"); } } else { ##### Relative filename my($abs_file) = find_file_in_path($file); if ($abs_file) { info 1, "Found $file at $abs_file\n"; $Included{$abs_file} = 1; } else { cf_warn($line, "Didn't find $file anywhere in path"); } } } # End of FILE loop } # End of LINE loop info 0, "\nDone with $contents_file\n\n"; if ($::disk_set eq "base+extra") { include_file(find_file_in_path("tar")) } close(CONTENTS) or error("close on $contents_file: $!"); ############################################################################## info 0, "\n\nPASS 2: Picking up extra files from links...\n"; for (keys %Included) { include_file($_); } info 0, "Done.\n\n"; ############################################################################## info 0, "PASS 3: Checking library dependencies...\n"; info 1, "(Ignore any 'statically linked' messages.)\n"; # Normal file X: X in %Included. # X -> Y: X in %links_to, Y in %Included # X <= Y: X in %Included and %replaced_by my(%strippable); my(%lib_needed_by); my($file); foreach $file (keys %Included) { ##### Use replacement file if specified $file = $replaced_by{$file} if defined($replaced_by{$file}); ##### Skip links (target will be checked) next if defined($links_to{$file}); # Symbolic (declared) next if -l $file; # Symbolic (on disk) my($file_line) = `file $file`; ##### See whether it's strippable and make a note. ##### This will prevent us from wasting time later running objcopy ##### on binaries that are already stripped. if ($file_line =~ m/not stripped/) { $strippable{$file} = 1; } ##### See whether it's a module and mark the info for later ##### so that we strip it correctly. if ($file_line =~ m/relocatable/) { info 1, "Marking $file as a module\n"; $is_module{$file} = 1; } elsif ($file_line =~ m/shared object/) { ##### Any library (shared object) seen here was explicitly included ##### by the user. push(@{$lib_needed_by{$file}}, "INCLUDED BY USER"); } if (-f $file and -B _ and -x _ and $file_line =~ /executable/) { ##### EXECUTABLE LOADABLE BINARY ##### Run ldd to get library dependencies. foreach $line (`ldd $file`) { my($lib) = $line =~ / => (\S+)/; next unless $lib; my($abs_lib) = $lib; if ($lib =~ /not found/) { warning "File $file needs library $lib, which does not exist!"; } else { ##### Right-hand side of the ldd output may be a symbolic link. ##### Resolve the lib absolutely. ##### include_file follows links and adds each file; ##### the while loop makes sure we get the last. $abs_lib = $lib; include_file($lib); while (1) { if (defined($links_to{$abs_lib})) { $abs_lib = make_link_absolute($abs_lib, $links_to{$abs_lib}); } if (defined($replaced_by{$abs_lib})) { $abs_lib = $replaced_by{$abs_lib}; } last unless -l $abs_lib; my($link) = readlink($abs_lib) or error "readlink($abs_lib): $!"; $abs_lib = make_link_absolute($abs_lib, $link); } } if (!defined($lib_needed_by{$abs_lib})) { info 0, "\t$abs_lib\n"; } push(@{$lib_needed_by{$abs_lib}}, $file); } } } ############################################################################## ##### Check libraries and loader(s) ##### ############################################################################## my(@Libs) = keys %lib_needed_by; my($seen_ELF_lib, $seen_AOUT_lib); my(%full_name); if (@Libs) { info 1, "\nYou need these libraries:\n"; my($lib); foreach $lib (@Libs) { my($size) = bytes_to_K(-s $lib); my($line) = " " x 15; my($file_output) = `file $lib`; if ($file_output =~ m/symbolic link/) { error "Yiiiiii, library file $lib is a symbolic link!\n", "This shouldn't happen!\n", "Please report this error to the Yard author\n"; } my($lib_type) = $file_output =~ /:\s*(ELF|Linux)/m; ##### All libraries are strippable $strippable{$lib} = 1; info 1, "$lib (type $lib_type, $size K) needed by:\n"; my($binary); for $binary (sort map(basename($_), @{$lib_needed_by{$lib}})) { if (length($line) + length($binary) > 78) { info 1, $line, "\n"; $line = " " x 15; } $line .= $binary . " "; } info 1, $line, "\n" if $line; if (!($seen_ELF_lib and $seen_AOUT_lib)) { ##### Check library to make sure we have the right loader. ##### (A better way is to do "ldconfig -p" and parse the output) ##### Strings from /usr/lib/magic of file 3.19 if (!defined($lib_type)) { error "Didn't understand `file` output for $lib:\n", `file $lib`, "\n"; } elsif ($lib_type eq 'ELF') { $seen_ELF_lib = 1; } elsif ($lib_type eq 'Linux') { # ie, a.out $seen_AOUT_lib = 1; } } ##### See if some other version of this library file is ##### being loaded, eg libc.so.3.1.2 and libc.so.5.2.18. ##### Not an error, but worth warning the user about. my($lib_stem) = basename($lib) =~ /^(.*?\.so)/; if (defined($full_name{$lib_stem})) { warning "You need both $lib and $full_name{$lib_stem}\n", "Check log file for details.\n"; } else { ##### eg, $full_name{"libc.so"} = "/lib/libc.so.5.2.18" $full_name{$lib_stem} = $lib; } } } info 1, "\n"; if ($seen_ELF_lib) { # There's no official way to get the loader file, AFAIK. # This expression should get the latest version, and Yard will grab any # hard-linked file. my($ld_file) = (yard_glob("/lib/ld-linux.so.?"))[-1]; # Get last one if (defined($ld_file)) { info 1, "Adding loader $ld_file for ELF libraries\n"; include_file($ld_file); } else { info 0, "Can't find ELF loader /lib/ld-linux.so.?"; } } if ($seen_AOUT_lib) { # Was: yard_glob("/lib/ld.so*") # Same as above, but ld.so seems to have no version number appended. my($ld_file); foreach $ld_file (yard_glob("/lib/ld.so")) { info 1, "Adding loader $ld_file for a.out libraries\n"; include_file($ld_file); } } info 0, "Done\n\n"; info 0, "PASS 4: Recording hard links...\n"; ##### Finally, scan all files for hard links. my(%hardlinked); foreach $file (keys %Included) { next if $links_to{$file} or $replaced_by{$file}; ##### $file is guaranteed to be absolute and not symbolically linked. ##### Record hard links on plain files if (-f $file) { my($dev, $inode, $mode, $nlink) = stat(_); if ($nlink > 1) { $hardlinked{$file} = "$dev/$inode"; } } } info 0, "Done.\n\n"; ############################################################################## info 0, "Checking space needed.\n"; my($total_bytes) = 0; my(%counted); foreach $file (keys %Included) { my($replacement, $devino); if ($replacement = $replaced_by{$file}) { ##### Use the replacement file instead of this one. In the ##### future, improve this so that replacement is resolved WRT ##### %links_to info 1, "Counting bytes of replacement $replacement\n"; $total_bytes += bytes_allocated($replacement); } elsif (-l $file or $links_to{$file}) { ##### Implicit or explicit symbolic link. Only count link size. ##### I don't think -l test is needed. my($size) = (-l $file) ? length(readlink($file)) : length($links_to{$file}); info 1, "$file (link) size $size\n"; $total_bytes += $size; } elsif ($devino = $hardlinked{$file}) { ##### This file is hard-linked to another. We don't necessarily ##### know that the others are going to be in the file set. Count ##### the first and mark the dev/inode so we don't count it again. if (!$counted{$devino}) { info 1, "Counting ", -s _, " bytes of hard-linked file $file\n"; $total_bytes += bytes_allocated($file); $counted{$devino} = 1; } else { info 1, "Not counting bytes of hard-linked file $file\n"; } } elsif (-d $file) { $total_bytes += $INODE_SIZE; info 1, "Directory $file = ", $INODE_SIZE, " bytes\n"; } elsif ($file =~ m|^/proc/|) { ##### /proc files screw us up (eg, /proc/kcore), and there's no ##### Perl file test that will detect them otherwise. next; } elsif (-f $file) { ##### Count space for plain files info 1, "$file size ", -s _, "\n"; $total_bytes += bytes_allocated($file); } } # Libraries are already included in the count info 0, "Total space needed is ", bytes_to_K($total_bytes), " Kbytes\n"; if (bytes_to_K($total_bytes) > $::fs_size) { info 0, "This is more than $::fs_size Kbytes allowed.\n"; if ($::strip_objfiles) { info 0, "But since object files will be stripped, more space\n", "may become available. Continuing...\n"; } else { error "You need to trim some files out and try again.\n"; } } info 0, "\n"; ############################################################################## ##### Create filesystem ############################################################################## sync(); sys("dd if=/dev/zero of=$::device bs=1k count=$::fs_size"); sync(); info 0, "Creating ${::fs_size}K ext2 file system on $::device\n"; if (-f $::device) { ##### If device is a plain file, it means we're using some loopback ##### device. Use -F switch in mke2fs so it won't complain. sys("mke2fs -F -m 0 -b 1024 $::device $::fs_size"); } else { sys("mke2fs -m 0 -b 1024 $::device $::fs_size"); } &mount_device; ##### lost+found on a ramdisk is pointless sys("rm -rf $::mount_point/lost+found"); sync(); ##### Setting up the file structure is tricky. Given a tangled set ##### of symbolic links and directories, we have to create the ##### directories, symlinks and files in the right order so that no ##### dependencies are missed. ##### First, create directories for symlink targets that are supposed ##### to be directories. Symlink targets can't be superseded so ##### sorting them by path length should give us a linear ordering. info 0, "Creating directories for symlink targets\n"; for $file (sort { path_length($a) <=> path_length($b) } keys %links_to) { my($link_target) = $links_to{$file}; my($abs_file) = make_link_absolute($file, $link_target); if (-d $abs_file) { my($floppy_file) = $::mount_point . $abs_file; my($newdir); foreach $newdir (mkpath($floppy_file)) { info 1, "\tCreating $newdir as a link target for $file\n"; } } } ##### Next, set up actual symlinks, plus any directories that weren't ##### created in the first pass. Sorting by path length ensures that ##### parent symlinks get set up before child traversals. info 0, "Creating symlinks and remaining directories.\n"; for $file (sort { path_length($a) <=> path_length($b) } keys %Included) { my($target); if (defined($target = $links_to{$file})) { my($floppy_file) = $::mount_point . $file; mkpath(dirname($floppy_file)); info 1, "\tLink\t$floppy_file -> $target\n"; symlink($target, $floppy_file) or error "symlink($target, $floppy_file): $!\n"; delete $Included{$file}; # Get rid of it so next pass doesn't copy it } elsif (-d $file) { my($floppy_file) = $::mount_point . $file; my($newdir); foreach $newdir (mkpath($floppy_file)) { info 1, "\tCreate\t$newdir\n"; } delete $Included{$file}; # Get rid of it so next pass doesn't copy it } } ##### Tricky stuff is over with, now copy the remaining files. info 0, "\nCopying files to $::device\n"; my(%copied); my($file); while (($file) = each %Included) { my($floppy_file) = $::mount_point . $file; my($replacement); if (defined($replacement = $replaced_by{$file})) { $file = $replacement; } if ($file =~ m|^/proc/|) { ##### Ignore /proc files next; } elsif (-f $file) { ##### A normal file. mkpath(dirname($floppy_file)); ##### Maybe a hard link. my($devino, $firstfile); if (defined($devino = $hardlinked{$file})) { ##### It's a hard link - see if the linked file is already ##### on the root filesystem. if (defined($firstfile = $copied{$devino})) { ##### YES - just hard link it to existing file. info 1, "Hard linking $floppy_file to $firstfile\n"; sys("ln $firstfile $floppy_file"); next; # Skip copy } else { ##### NO - copy it. $copied{$devino} = $floppy_file; } } info 1, "$file -> $floppy_file\n"; copy_strip_file($file, $floppy_file); } elsif (-d $file) { ##### A directory. info 1, "Creating directory $floppy_file\n"; mkpath($floppy_file); } elsif ($file eq '/dev/null' and $floppy_file ne "$::mount_point/dev/null") { # I hate this info 1, "Creating empty file $floppy_file\n"; mkpath(dirname($floppy_file)); sys("touch $floppy_file"); } else { ##### Some special file. info 1, "Copying special $file to $floppy_file\n"; mkpath(dirname($floppy_file)); # The 'R' flag here allows cp command to handle devices and FIFOs. sys("cp -dpR $file $floppy_file"); } } ############################################################################## info 0, "\nFinished creating root filesystem.\n"; if (@Libs) { info 0, "Re-generating /etc/ld.so.cache on root fs.\n"; info 1, "Ignore warnings about missing directories\n"; sys("ldconfig -v -r $::mount_point"); } info 0, "\nDone with $PROGRAM_NAME. $Warnings warnings.\n", "$::device is still mounted on $::mount_point\n"; exit( $Warnings>0 ? -1 : 0); ############################################################################# ##### Utility subs for make_root_fs.pl ############################################################################# ##### Add file to the file set. File has to be an absolute filename. ##### If file is a symlink, add it and chase the link(s) until a file is ##### reached. sub include_file { my($file) = @_; must_be_abs($file); if (onto_proc_filesystem($file)) { info 1, "File $file points into proc filesystem -- not pursued.\n"; return; } $Included{$file} = 1; ##### If we have links A -> B -> C -> D -> E ##### on disk and A -> D is set explicitly, then we pick up ##### files A and D in pass 1, and E on pass 2. while (!defined($links_to{$file}) and !defined($replaced_by{$file}) and -l $file) { ##### SYMBOLIC LINK on disk, not overridden by explicit link or ##### replacement. Relativize the link for use later, but also ##### check and resolve the target so it gets onto the rescue disk. my($link) = readlink($file) or error "readlink($file): $!"; my($rel_link) = make_link_relative($file, $link); $links_to{$file} = $rel_link; my($abs_target) = make_link_absolute($file, $link); if (onto_proc_filesystem($abs_target)) { info 1, "$file points to $abs_target, on proc filesystem\n"; last; } if (!$Included{$abs_target}) { info 1, "File $file is a symbolic link to $link\n"; info 1, "\t(which resolves to $abs_target),\n" if $link ne $abs_target; info 1, "\twhich was not included in $::contents_file.\n"; if (-e $abs_target) { info 1, "\t ==> Adding it to file set.\n\n"; $Included{$abs_target} = $file; } else { info 0, "\t ==> $abs_target does not exist. Fix this!\n"; } } $file = $abs_target; # For next iteration of while loop } } ##### More informative versions of warn and die, for the contents file sub cf_die { my($line, @msgs) = @_; info 0, "$::contents_file($cf_line): $line\n"; foreach (@msgs) { info 0, "\t$_\n"; } exit; } sub cf_warn { my($line, @msgs) = @_; info 0, "$::contents_file($cf_line): $line\n"; $Warnings++; foreach (@msgs) { info 0, "\t$_\n"; } } # Copy a file, possibly stripping it. Stripping is done if the file # is strippable and stripping is desired by the user, and if the # objcopy program exists. sub copy_strip_file { my($from, $to) = @_; if ($::strip_objfiles and defined($objcopy) and $strippable{$from}) { # Copy it stripped if (defined($lib_needed_by{$from})) { # It's a library info 1, "Copy/stripping library $from to $to\n"; sys("$objcopy --strip-all $from $to"); } elsif (defined($is_module{$from})) { info 1, "Copy/stripping module $from to $to\n"; sys("$objcopy --strip-debug $from $to"); } else { # It's a binary executable info 1, "Copy/stripping binary executable $from to $to\n"; sys("$objcopy --strip-all $from $to"); } # Copy file perms and owner my($mode, $uid, $gid); (undef, undef, $mode, undef, $uid, $gid) = stat $from; chown($uid, $gid, $to) or error "chown: $!"; chmod($mode, $to) or error "chmod: $!"; } else { # Normal copy, no strip sys("cp $from $to"); } } ##### End of make_root_fs ############################################################## ############################################################## ############################################################## ############################################################################## ## ## CHECK_ROOT_FS ## ############################################################################## BEGIN { require "yard_utils.pl" } require "Config.pl"; ### GLOBAL VARIABLES my(%Termcap); # Defs from /etc/termcap my($checked_for_getty_files); # Scalar -- have we checked getty files yet? my(%checked); # Hash table of files we've already checked # This is a little crude. Technically we should read /etc/conf.getty # to make sure we're not supposed to be using a different login binary. my($login_binary) = "$::mount_point/bin/login"; STDOUT->autoflush(1); start_logging_output(); info 0, "check_root_fs @yard_version@\n"; mount_device_if_necessary(); # This goes first so we define %Termcap for use in children check_termcap(); ##### Here are the tests. fork_chroot_and(\&check_fstab); fork_chroot_and(\&check_inittab); fork_chroot_and(\&check_scripts); check_links(); check_passwd(); check_pam(); check_nss(); info 0, "All done.\n"; info 0, "If this is acceptable, continue with write_rescue_disk\n"; exit; ############################################################################## sub warning { info 0, "\n", @_; # $Warnings++; } # This takes a procedure call, forks off a subprocess, chroots to # $::mount_point and runs the procedure. sub fork_chroot_and { my($call) = @_; my($Godot) = fork; die "Can't fork: $!" unless defined $Godot; if (!$Godot) { # Child process chdir($::mount_point); chroot($::mount_point); ##### chroot to the root filesystem &$call; exit; } else { # Parent here waitpid($Godot, 0); } } sub check_fstab { my($FSTAB) = "/etc/fstab"; my($proc_seen); open(FSTAB, "<$FSTAB") or error "$FSTAB: $!"; info 0, "\nChecking $FSTAB\n"; while () { chomp; next if /^\#/ or /^\s*$/; my($dev, $mp, $type, $opts) = split; next if $mp eq 'none' or $type eq 'swap'; next if $dev eq 'none'; if (!-e $mp) { info 0, "$FSTAB($.): $_\n\tCreating $mp on root filesystem\n"; mkpath($mp); } if ($dev !~ /:/ and !-e $dev) { warning "$FSTAB($.): $_\n\tDevice $dev does not exist " . "on root filesystem\n"; } ##### If you use the file created by create_fstab, these tests ##### are superfluous. if ($dev =~ m|^/dev/hd| and $opts !~ /noauto/) { warning "\t($.): You probably should include \"noauto\" option\n", "\tin the fstab entry of a hard disk. When the rescue floppy\n", "\tboots, the \"mount -a\" will try to mount $dev\n"; } elsif ($dev eq $::floppy and $type ne 'ext2' and $type ne 'auto') { warning "\t($.): You've declared your floppy drive $::floppy", " to hold\n", "\ta $type filesystem, which is not ext2. The rescue floppy\n", "\tis ext2, which may confuse 'mount -a' during boot.\n"; } elsif ($type eq 'proc') { $proc_seen = 1; } } close(FSTAB); warning "\tNo /proc filesystem defined.\n" unless $proc_seen; info 0, "Done with $FSTAB\n"; } sub check_inittab { my($INITTAB) = "/etc/inittab"; info 0, "\nChecking $INITTAB\n"; if (!open(INITTAB, "<$INITTAB")) { warning "$INITTAB: $!\n"; return } my($default_rl, $saw_line_for_default_rl); while () { chomp; my($line) = $_; # Copy for errors s/\#.*$//; # Delete comments next if /^\s*$/; # Skip empty lines my($code, $runlevels, $action, $command) = split(':'); if ($action eq 'initdefault') { ##### The initdefault runlevel $default_rl = $runlevels; next; } if ($runlevels =~ /$default_rl/) { $saw_line_for_default_rl = 1; } if ($command) { my($exec, @args) = split(' ', $command); if (!-f $exec) { warning "$INITTAB($.): $line\n", "\t$exec: non-existent or non-executable\n"; } elsif (!-x $exec) { info 0, "$INITTAB($.): $line\n", info 0, "\tMaking $exec executable\n"; chmod(0777, $exec) or error "chmod failed: $!"; } else { ##### executable but not binary ==> script scan_command_file($exec, @args) if !-B $exec; } if ($exec =~ m|getty|) { # matches *getty* call check_getty_type_call($exec, @args); } } } close(INITTAB) or error "close(INITTAB): $!"; if (!$saw_line_for_default_rl) { warning "\tDefault runlevel is $default_rl, but no entry for it.\n"; } info 0, "Done with $INITTAB\n"; } ##### This could be made much more complete, but for typical rc type ##### files it seems to catch the common problems. sub scan_command_file { my($cmdfile, @args) = @_; my(%warned, $line); return if $checked{$cmdfile}; info 0, "\nScanning $cmdfile\n"; open(CMDFILE, "<$cmdfile") or error "$cmdfile: $!"; while ($line = ) { chomp($line); next if $line =~ /^\#/ or /^\s*$/; next if $line =~ /^\w+=/; while ($line =~ m!(/(usr|var|bin|sbin|etc|dev)/\S+)(\s|$)!g) { my($abs_file) = $1; # next if $abs_file =~ m/[*?]/; # Skip meta chars - we don't trust glob next if $warned{$abs_file}; # Only warn once per file if (!-e $abs_file) { warning "$cmdfile($.): $line\n\t$1: missing on root filesystem\n"; $warned{$abs_file} = 1; } } } close(CMDFILE) or error "close($cmdfile): $!"; $checked{$cmdfile} = 1; info 0, "Done scanning $cmdfile\n"; } ##### Check_passwd is NOT run under chroot. sub check_passwd { my($passwd_file) = "$::mount_point/etc/passwd"; open(PASSWD, "<$passwd_file") or error "Can't read passwd file: $!\n"; info 0, "\nChecking passwd file $passwd_file\n"; while () { chomp; next if /^\s*$/; # Skip blank/empty lines my($line) = $_; my($login_name, $passwd, $UID, $GID, $user_name, $home, $shell) = split(':'); next if $passwd eq "*"; # Skip warnings if user can't login -d ($::mount_point . $home) or warning "$passwd_file($.): $line\n", "\tHome directory of $login_name ($::mount_point$home) is missing\n"; -e ($::mount_point . $shell) or warning "$passwd_file($.): $line\n", "\tShell of $login_name ($::mount_point$shell) doesn't exist\n"; check_init_files($login_name, $home, $shell); } close(PASSWD); info 0, "Done checking $passwd_file\n"; } ##### Simple PAM configuration checks. ##### Tests whether PAM is needed, and whether the configuration libraries exist. ##### Check_pam is NOT run under chroot. sub check_pam { my($pam_configured) = 0; # Have we seen some pam config file yet? info 0, "Checking for PAM\n"; my($pamd_dir) = "$::mount_point/etc/pam.d"; my($pam_conf) = "$::mount_point/etc/pam.conf"; if (-e $pam_conf) { info 0, "Checking $pam_conf\n"; $pam_configured = 1; open(PAM, $pam_conf) or error "Can't open pam.conf: $!\n"; while () { chomp; next if /^\#/ or /^\s*$/; # Skip comments and empty lines my($file) = (split)[3]; # Get fourth field if (!-e "$::mount_point/$file") { warning "$pam_conf($.): $_\n", "\tLibrary $file does not exist on root fs\n"; } # That's all we check for now } close(PAM) or die "Closing PAM: $!"; info 0, "Done with $pam_conf\n"; } if (-e $pamd_dir) { info 0, "Checking files in $pamd_dir\n"; opendir(PAMD, $pamd_dir) or error "Can't open $pamd_dir: $!"; my($file); while (defined($file = readdir(PAMD))) { my($file2) = "$pamd_dir/$file"; next unless -f $file2; # Skip directories, etc. open(PF, $file2) or error "$file2: $!"; while () { chomp; next if /^\#/ or /^\s*$/; # Skip comments and empty lines my($file) = (split)[3]; # Get fourth field $pam_configured = 1; if (!-e "$::mount_point/$file") { warning "$file2($.): $_\n", "\tLibrary $file does not exist on root fs\n"; } } close(PF); } closedir(PAMD); } # Finally, see whether PAM configuration is needed if (!$pam_configured and -e $login_binary) { my($dependencies) = scalar(`ldd $login_binary`); if (defined($dependencies) and $dependencies =~ /libpam/) { warning "Warning: login ($login_binary) needs PAM, but you haven't\n", "\tconfigured it (in /etc/pam.conf or /etc/pam.d/)\n", "\tYou probably won't be able to login.\n"; } } info 0, "Done with PAM\n"; } ##### Basic checks for nsswitch.conf. ##### check_nss is NOT run under chroot. ##### From the nsswitch.conf(5) manpage: ##### For glibc, you must have a file called /lib/libnss_SERVICE.so.X for ##### every SERVICE you are using. On a standard installation, you could ##### use `files', `db', `nis' and `nisplus'. For hosts, you could specify ##### `dns' as extra service, for passwd, group and shadow `compat'. These ##### services will not be used by libc5 with NYS. The version number X ##### is 1 for glibc 2.0 and 2 for glibc 2.1. sub check_nss { my($nss_conf) = "$::mount_point/etc/nsswitch.conf"; info 0, "Checking for NSS\n"; my($libc) = yard_glob("$::mount_point/lib/libc-2*"); my($libc_version) = $libc =~ m|/lib/libc-2.(\d)|; if (!defined($libc_version)) { warning "Can't determine your libc version\n"; } else { info 0, "You're using $libc\n"; } my($X) = $libc_version + 1; if (-e $nss_conf) { open(NSS, "<$nss_conf") or die "open($nss_conf): $!"; my($line); while (defined($line = )) { chomp $line; next if $line =~ /^\#/; next if $line =~ /^\s*$/; my($db, $entries) = $line =~ m/^(\w+):\s*(.+)$/; # Remove bracketed expressions (action specifiers) $entries =~ s/\[[^\]]*\]//g; my(@entries) = split(' ', $entries); my($entry); for $entry (@entries) { next if $entry =~ /^\[/; # ignore action specifiers my($lib) = "$::mount_point/lib/libnss_${entry}.so.${X}"; if (!-e $lib) { warning "$nss_conf($.):\n$line\n", "\tRoot filesystem needs $lib to support $entry\n"; } } } } else { # No nsswitch.conf is present, figure out if maybe there should be one. if (-e $login_binary) { my($dependencies) = scalar(`ldd $login_binary`); my($libc_version) = ($dependencies =~ /libc\.so\.(\d+)/m); if ($libc_version > 5) { # Needs libc 6 or greater warning "Warning: $login_binary on rescue disk needs libc.so.$libc_version,\n" . "\tbut there is no NSS configuration file ($nss_conf)\n" . "\ton root filesystem.\n"; } } } info 0, "Done with NSS\n"; } sub check_links { info 0, "\nChecking links relative to $::mount_point\n"; sub wanted { if (-l $File::Find::name) { local($raw_link) = readlink($File::Find::name); local($target) = make_link_absolute($File::Find::name, $raw_link); # I added this next test for /dev/stdout link hair. # This really should be more complicated to handle link chains, # but as a hack this works for three. if (onto_proc_filesystem($File::Find::name)) { } elsif (-l $target) { chase_link($target, 16); } elsif (!-e $target) { warning "Warning: Unresolved link: $File::Find::name -> $raw_link\n"; } } }; finddepth(\&wanted, $::mount_point); } sub chase_link { my($file, $link_depth) = @_; if ($link_depth == 0) { warning "Warning: Probable link circularity involving $file\n"; } elsif (-l $file) { chase_link(make_link_absolute($file, readlink($file)), $link_depth-1); } } sub check_scripts { info 0, "\nChecking script interpreters\n"; local($prog); sub check_interpreter { if (-x $File::Find::name and -f _ and -T _) { open(SCRIPT, $File::Find::name) or error "$File::Find::name: $!"; my($prog, $firstline); chomp($firstline =