#!/usr/bin/perl # # This document is in text, HTML, and perl script format. sub __GNUC__ { 1 } $|=1; # prevent stderr/stdout conflicts; # /tmp cleaner script by zblaxell@myrus.com (Zygo Blaxell) # Version 2.2, 96/05/23 # Wrote some more docs and cleaned up a spelling error # ('NOATIME' was spelled 'ATIME'). # Version 2.11, 96/05/08 # Added 'ADD_DATE' variable # Version 2.1, 96/05/07 # Fixed long-standing stat_fs bug # Version 2.0, 96/05/06 # Fairly major restructuring. Now the children are forked on initial # startup, whether reaping is necessary or not. The logging # information is reorganized. # The specification for threshold space levels is now one of: # number - percentage of disk # number K - kilobytes # number M - megabytes # Version 1.31, 96/05/01 # Only print STDERR "Disk usage increased:" if QUIET is not set. # Version 1.3, 96/04/26 # If we have a dangling symlink, ignore its atime. We keep changing it # with readlink(). # Version 1.24, 96/04/25 # Changes to constant strings in output and comments. I was going to # make a change, but then decided that the status quo is a feature. # Version 1.23, 96/04/12 # Use 'localtime()' more often to print out dates. No change in behavior. # Version 1.22, 96/04/10 # Corrects a couple of reporting bugs (negative disk space and incorrect # file size). No change in behavior. ######################################################################## ######################################################################## ######################################################################## # No warranties express or implied; see the GNU GPL for copying # restrictions. # You may get a copy of the software licence from: # ftp://ftp.gnu.ai.mit.edu/pub/gnu/COPYING # This is filereaper, a stripped-down version of gfreaper. # Actually, it's partly stripped-up too. # This script is designed to maintain a particular amount of free # disk space on a partition by deleting files in a directory structure. # For example, if you wanted to always have 3% free space in /tmp, use: # filereaper 3 /tmp # Files are deleted in order approximated by "oldest files first". # Actually, there are some anomalies where symbolic links and # directories are concerned. Directories are considered one second # older than the oldest file in them. Symbolic links are considered one # second newer than the file that they point to, or their own age if they # don't point to a file. Directories can be maintained by typing 'ln -s # . ...' in them--this will delete everything in the directory, of course, # but never the directory itself (actually, filereaper never deletes the # symlink, thus preserving the directory which never gets emptied). # This program also maintains some state between reapings that allows it # to run more efficiently. It can begin deleting files within two # seconds of low-disk-space conditions occurring. # This program understands some security issues that many other programs # (and some sysadmins) don't. I don't know how many times I have seen: # 0 * * * * find /tmp -mtime +1 -exec rm -f {} \; # or, even worse: # 0 * * * * find /tmp -mtime +1 -print | xargs rm -f # in crontab files running as root. # The problem with these is that they can be used by any hostile user # with write access to /tmp to delete any file on the filesystem. In the # second case, the exploit is trivially easy. To delete /etc/passwd and # /etc/group: # $ touch '/tmp/this filename contains whitespace and newlines # /etc/passwd # /etc/group' # In the first case, you need to exploit a race condition between find # and rm (which is exacerbated by xargs, I might add). Consider a file # named '/tmp/foo/bar/baz/a/b/c/d/e/etc/passwd'. The race condition is # exploited thus: # [find feeds '/tmp/foo/bar/baz/a/b/c/d/e/etc/passwd' to 'rm'] # $ mv /tmp/foo/bar/baz/a/b/c/d/e/etc /tmp/e # $ ln -s /etc /tmp/foo/bar/baz/a/b/c/d/e/etc # [rm now deletes '/tmp/foo/bar/baz/a/b/c/d/e/etc/passwd', but the # directory '/tmp/.../etc' is now a symlink to the real '/etc', so in fact # rm will actually delete '/etc/passwd'.] # With some creativity it is possible to make stat() calls take several # minutes, so this race condition is not difficult to exploit. Consider # what happens when you call stat() on a path with 500 directory # components, each of which is actually a symlink 8 levels deep through # 500 other directory components. Each. We're talking about reading # almost the entire inode table into memory here just to do ONE stat() # call, and that can't be very fast. The attacker just moves the first # directory component aside and puts a symlink to '/etc/' (or wherever) in # its place, so the actual unlink() call to the wrong file will be nice # and fast. # Incidentally, since these examples use '-mtime', the file modification # time is used to schedule file deletion. Since the file modification # time is entirely under the control of the user, the user can delete any # file they want deleted, when they want it deleted, and they can # prevent their own files from being deleted. Use '-ctime' instead. If # your 'find' doesn't have this feature, 'rm -f /usr/bin/find' (in case # someone else tries to use it) and get one of the freely-available # replacements, or write your own. # ONCE AGAIN: DO NOT USE: # 0 * * * * find /tmp -mtime +1 -exec rm -f {} \; # IN ROOT'S CRONTAB. YOU WILL ALLOW ANY USER TO DELETE ANY FILE ON THE # SYSTEM. THIS IS A SECURITY HOLE THAT IS EASY TO EXPLOIT. There, that # should place this document in a few full-text search engines. :-) # There are alternatives to this that work. One is: # 0 * * * * find /tmp -mtime +1 -exec safe_rm -f {} \; # where 'safe_rm' implements an algorithm similar to 'ch_dir' in this # program. The algorithm is: # If any system call below fails, exit. # chdir("/"); # split name into a list of path components and a filename. The # filename must be non-empty and does not contain "/". # for each member of list: # old1=lstat(member) (save the device and inode numbers in old1) # old2=lstat(".") (save the device and inode numbers in old2) # chdir(member) # new1=lstat(".") # new2=lstat("..") # if (old1 != new1 || old2 != new2) exit # next member # unlink or rmdir (filename) # This avoids the race condition by not following symlinks in the # directory components of the path name. Attempts to exploit the # find/unlink race condition will fail because the ch_dir routine will # fail if it encounters a symbolic link in the directory components of # the path name. # This is not perfect; however, it is necessary for the user to be able # to write to parent directories in a path name in order to subvert files # later in a path name. For instance, consider: # /tmp/user1/ # /tmp/foo/bar/root/user2 # where 'user1' is owned by user1, 'root' is owned by root and not # writable, and 'user2' is a file owned by user2. User1 cannot delete # '/tmp/foo/bar/root/user2', because 'root' is not writable by user1. # However, User1 can exploit the security vulnerabiltiies left in the # algorithm above to cause the daemon to delete /tmp/foo/bar/root/user2 IF # AND ONLY IF /tmp/foo and /tmp/foo/bar are writable by user2, as follows: # 1. Create files and directories such that '/tmp/user1/bar/root/user2' # exists. # 2. Wait for 'find' to output the name '/tmp/user1/root/user2'. # 3. 'mv /tmp/user1/bar /tmp/user1/BAR' # 4. 'mv /tmp/foo/bar /tmp/user1'. This moves # '/tmp/foo/bar/root/user2' to '/tmp/user1/bar/root/user2'. # 5. The safe_rm will now delete '/tmp/user1/bar/root/user2'. Because # there are no symlinks, no anomalies will be detected. # This program has additional checks to try to prevent attacks like this. # It can still be exploited if /tmp/user1/bar/root/user2 is a hard link # to /tmp/foo/bar/root/user2. # The moral of the story is: if someone can write to your parent # directory, all bets are off. # Note that in real life, /tmp should be mode 1777, i.e. it has the # sticky bit set. In this case /tmp/foo has to be owned by user1 for # the exploit to work. RTFM chmod(1,2) if you want to know why. # This program also does chroot() to the base of filesystems to be reaper, # if run as root, for extra security. ######################################################################## ######################################################################## ######################################################################## # BUGS AND PORTABILITY ISSUES # If your filesystem isn't a true blue Unix system, there may be problems. # All inodes are assumed to be unique and constant. If your filesystem # generates inode numbers to be compatible with POSIX, and this # generation is not repeatable, then this program will scream about # files constantly changing before they can get deleted. Some # my-toy-filesystem-to-NFS gateways create and cache inode numbers # on the fly; if not accessed frequently, inode numbers of files go stale. # Filesystems that don't have a notion of 'ctime' or 'atime' or user IDs # (such as the MSDOS filesystem) can cause problems. The implementation # of MSDOS filesystem that I use has an 'mtime' stamp, and uses it for all # three timestamp values. This means that simply reading a file # doesn't 'touch' it, because there is no 'atime' field to update. Also, # since writing the 'mtime' field sets the 'ctime' field to the same # value, we have no idea whether a file is old or new, or just dated # that way, and the order in which files are deleted becomes very # arbitrary. Finally, if the default UID of the MSDOS filesystem is # root, you'll have to set ROOTAGE to a large negative value for it to # work, because people can set the "ctime" of files to far into the future. # I set the UID and GID for MSDOS filesystems to a non-root user # named 'msdos' to work around this problem. ######################################################################## ######################################################################## ######################################################################## # Here's a sample script to run from /etc/rc* at boot time. Remove '### ' # from the beginning of every line. ### #!/bin/sh ### # Filereaper sample script by Zygo Blaxell (C) 1996 ### # License: ftp://ftp.gnu.ai.mit.edu/pub/gnu/COPYING ### ### # Prevent core dumps. Those would be bad, even when chroot. ### ulimit -c 0 ### # Set PATH so we can find filereaper ### PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin ### ### { ### # Delete old files in spool and temp directories if <1% free space. ### # The /*/windows/temp tries to get /[CD]/windows/temp on our systems. ### # Yes, we've been forced to use Windows '95, hence /*/recycled. ### # When we have multiple partitions, there is a /tmp on each. ### # You probably want this at elevated priority to prevent disks ### # from getting filled faster than they can be emptied, hence 'nice'. ### ### ADD_DATE=true QUIET=true INTERVAL=60 nice -n -20 filereaper 1 /*/tmp /tmp /*/recycled /*/windows/temp ### ### } >>/var/log/filereaper 2>&1 & ######################################################################## ######################################################################## ######################################################################## # Variables: # inode_info{inode} = age\0(parent_inode_number,name\0)+ # Information about an inode. # oldest_age = oldest timestamp of files we let stand. # root_inode = inode of highest-level directory to tmpclean (used to # stop searching backwards) # root_dev = device number of root of filesystem (used to stop searching # other filesystems) # root_path = string to prepend to all pathnames ("" if chroot, # "/foo/bar/baz" if not). # min_spec = amount of space to keep free on filesystem (%, K, or M) # ROOTAGE = minimum age of a file owned by root before we delete it. # Use this if you have daemons writing stuff to /tmp that would be # annoyed, destructive, or vulnerable if their /tmp files go away. # Return values of ch_dir(): $CH_DIR_SUCCESS=0; $CH_DIR_CROSS=1; $CH_DIR_ERROR=2; ########################################################################### ########################################################################### ########################################################################### # Initialization ($min_spec,@given_filesystems)=@ARGV; die <($min_spec =~ m/^\d+[%km]?$/i); Usage: $0 _to_keep_free> [ [ [...]]] Deletes old files whenever the amount of free space on the filesystem drops below _to_keep_free>. The amount of space can be specified as one of the following: - percentage of total disk space available to users % - same as K - number of kilobytes M - number of megabytes Environment Variables (set them if you want them): TEST - test only, don't actually delete any files (default of course is to really delete files) DEBUG - print debugging information (default is to print only warnings and fatal and non-fatal errors, as well as the name of every file we try to delete) NOATIME - ignore the atime of files, use ctime only. (default is to use the later of atime and ctime for non-directories. mtime can be set by the file owner to any value, so it's ignored. With NOATIME, simply reading a file doesn't help preserve it) ROOTAGE - minimum age of files owned by root in seconds. (by default, any file is eligible for deletion unless that file is owned by root and it is less than $ROOTAGE seconds old) MINAGE - minimum age applies to all users, not just root (boolean). (by default, files not owned by root are always eligible for deletion) INTERVAL- interval, in seconds, between FS checks. Default 60. Negative values mean run only once. QUIET - don't print messages that explain why we aren't doing something. NODIRS - don't preserve directories using 'ln -s . ...'. By default, if a directory contains the symlink '...' with target '.', it will never be deleted by this program. ADD_DATE- prefix each output line with date and time. USAGE srand(); $min_spec =~ s/\d$/$&\%/o; $TEST=$ENV{'TEST'}; $DEBUG=$ENV{'DEBUG'}; $NOATIME=$ENV{'NOATIME'}; $ROOTAGE=$ENV{'ROOTAGE'} || 24*60*60; $MINAGE=$ENV{'MINAGE'}; $INTERVAL=$ENV{'INTERVAL'} || 60; $QUIET=$ENV{'QUIET'}; $NODIRS=$ENV{'NODIRS'}; $ADD_DATE=$ENV{'ADD_DATE'}; print STDERR "$0 - parameter dump at ".localtime(time()).":\n"; print STDERR $> ? "Running as uid $>--can't chroot, will try to manage without it\n" : "Running as root, can and will do chroot\n"; print STDERR $TEST ? "Running in test mode only\n" : "This is not a test.\n"; print STDERR "Debugging messages enabled\n" if $DEBUG; print STDERR "Will maintain $min_spec free space\n"; print STDERR $NOATIME ? "Will ignore atime of files\n" : "Will honour atime of files\n"; print STDERR "Minimum age of files owned by ", $MINAGE ? "all users" : "root", " is $ROOTAGE seconds.\n"; print STDERR "Will check filesystems every $INTERVAL seconds.\n"; print STDERR $QUIET ? "Will suppress spurious filesystem status messages\n" : "Will print STDERR spurious filesystem status messages\n"; print STDERR $NOATIME ? "Will ignore symlinks to '.' named '...'\n" : "Will not delete directories containing symlinks to '.' named '...'\n"; print STDERR "Reading passwd and group files...\n"; setpwent(); while ( ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell) = getpwent ) { $username_of{$uid}=$name unless defined($username_of{$uid}); } endpwent(); setgrent(); while ( ($name,$passwd,$gid,$members) = getgrent ) { $groupname_of{$gid}=$name unless defined($groupname_of{$gid}); } endgrent(); print STDERR "Done reading passwd and group files.\n"; $last_ch_dir="\0"; # Really go to work now... print STDERR "Entering main loop\n"; $SIG{'CHLD'}="IGNORE"; while (1) { #print STDERR "Reaping filesystems at ".localtime()."\n" unless $QUIET; undef @filesystems; foreach (@given_filesystems) { local($chdir_result)=&ch_dir($_); if ($chdir_result) { # Arguably, the status of paths on the command line may change... print STDERR "$_ could not be found or crossed symlinks--will not try again.\n"; next; } push(@filesystems,$_); # Grim File Reaper... if ($fs_to_pid{$_}) { print STDERR "Still running: pid $fs_to_pid{$_}, fs $_\n" unless $QUIET; next; } $parent_pid=$$; $child=fork(); unless (defined($child)) { warn "Didn't fork? $!"; next; } if (!($child)) { # Child of fork() $other_parent_id=$$; unless (open(STDERR ,"|-")) { # Child of open() die "Didn't fork? $!" if ($$ == $other_parent_id); while ($in=) { if ($ADD_DATE) { print STDERR localtime()." $_: $in"; } else { print STDERR "$_: $in"; } } print STDERR "$_: Done\n" unless $QUIET; exit(0); } else { # parent of open() open(STDOUT,">&STDERR ") || die "Dup stderr: $!"; } &reap($_); exit(0); } else { # Parent of fork() #print STDERR "Reaping $_ in pid $child\n"; # Hardly spurious... $pid_to_fs{$child}=$_; $fs_to_pid{$_}=$child; } } @given_filesystems=@filesystems; #print STDERR "Idle.\n" unless $QUIET; sleep($INTERVAL) if $INTERVAL>=1; for $child (keys(%pid_to_fs)) { next if kill(0,$child); # test for existence #print STDERR "Child $child no longer exists, free to clean $pid_to_fs{$child} again.\n"; delete $fs_to_pid{$pid_to_fs{$child}}; delete $pid_to_fs{$child}; } last if $INTERVAL<0; } exit(0); # reap(directory); # Does the GFR bit in a given directory. Sets up inodes, sets up state # for directory trees. sub reap { ($root_path)=@_; local($begin_reaping)=time(); # reference point to "now" - saves time() calls &ch_dir($root_path) && die "chdir '$root_path' failed. Aborting.\n"; if ($>) { print STDERR "Not root, so can't chroot. Too bad.\n"; } else { print STDERR "Running as root. Doing chroot($root_path)..." unless $QUIET; local(@stat1)=(lstat("."))[0,1]; die "Couldn't lstat '.' in '$root_path': $!\n" unless defined($stat1[0]); chroot(".") || die "chroot failed ($!). Aborting.\n"; chdir("/") || die "chdir '/' failed in chroot ($!). Aborting.\n"; local(@stat2)=(lstat("."))[0,1]; die "Couldn't lstat '.' in '$root_path' after chroot: $!\n" unless defined($stat2[0]); local(@stat3)=(lstat(".."))[0,1]; die "Couldn't lstat '..' in '$root_path' after chroot: $!\n" unless defined($stat3[0]); local(@stat4)=(lstat("/"))[0,1]; die "Couldn't lstat '/' in '$root_path' after chroot: $!\n" unless defined($stat4[0]); # After chroot, '/' should be the same as '.' and '..', # which should be the same as '.' before chroot. die "I don't believe I have chrooted successfully. Aborting.\n" if (join(" ",@stat1) ne join(" ",@stat2) || join(" ",@stat2) ne join(" ",@stat3) || join(" ",@stat3) ne join(" ",@stat4)); print STDERR "Successfully chroot($root_path)\n" unless $QUIET; $root_path=''; $last_ch_dir="\0"; } ($root_dev,$root_inode)=(lstat("."))[0,1]; die "Couldn't lstat '.' in '$root_path': $!\n" unless defined($root_dev); print STDERR "Gathering filesystem information\n" unless $QUIET; &get_dir($root_path); local($want_before_reaping)=&stat_fs(); $oldest_age=time(); print STDERR "Waiting for go-ahead\n" unless $QUIET; local($want)=&stat_fs(); local($lastwant)=$want; until ($want>0) { sleep(2); $want=&stat_fs(); print STDERR (-$want)." bytes left\n" if ($want>$lastwant) && !$QUIET; $lastwant=$want if $lastwant<$want; } print STDERR "Now reaping files\n" unless $QUIET; inode: for $inode (sort { $inode_info{$a} <=> $inode_info{$b} } keys(%inode_info)) { local($inode_age,@other_stuff)=split(/\0/,$inode_info{$inode}); $oldest_age=$inode_age; local($want)=&stat_fs(); if ($want<=0) { # We hang around until we run out of inodes, # Then our parent starts us again. print STDERR "Want $want bytes...pausing...Oldest file is stamped ".localtime($oldest_age)."\n"; local($lastwant)=$want; until ($want>0) { sleep(2); $want=&stat_fs(); print STDERR (-$want)." bytes left\n" if ($want>$lastwant) && !$QUIET; $lastwant=$want if $lastwant<$want; } } $count=0; name: foreach $file_name (&get_names($inode)) { local($dir,$file)= $file_name =~ m!^(.*)/(.*)$!; ($was_link,$link_target,$was_dir)=&get_inode_info($dir,$file); next name unless defined($was_link); unless ($count++) { print STDERR "Want $want bytes, reaping inode $inode, age ".localtime($inode_age).", owner " . ($username_of{$ino_uid} || ":$ino_uid:") . ":" . ($groupname_of{$ino_gid} || ":$ino_gid:") . ", size " . ($link_blocks*512) . "\n"; if ( ( ! $ino_uid || $MINAGE ) && $inode_age > time()) { print STDERR "skipping: Inode $inode name '$dir/$file' is owned by " . ($username_of{$ino_uid} || ":$ino_uid:") . ":" . ($groupname_of{$ino_gid} || ":$ino_gid:") . " and not old enough (",time()-$inode_age,").\n"; next inode; } # Timestamps of directories are changed by gfreaper. unless ($was_dir) { local($new_age); if ($ino_atime>$ino_ctime && !$NOATIME && $ino_atime < time()+1) { $new_age=$ino_atime; } else { $new_age=$ino_ctime; } $new_age+=$ROOTAGE if ( ( ! $link_uid || $MINAGE )); $new_age-- if $was_link; print STDERR "old age $inode_age, new age $new_age\n" if $DEBUG; if ($new_age > $inode_age) { print STDERR "skipping: Inode $inode name '$dir/$file' age is newer (".localtime($new_age)." > ".localtime($inode_age).")\n"; next inode; } } } if ($link_dev != $root_dev) { warn "skipping: Inode $inode name '$dir/$file' is not on the correct filesystem ('$link_dev' should be '$root_dev').\n"; next name; } if ($link_ino != $inode) { warn "skipping: Name '$dir/$file' is associated with new inode ('$link_ino' should be '$inode')\n"; next name; } print STDERR "Reaping: $file_name\n"; if ($TEST) { print STDERR "Pretending to unlink $dir/$file\n"; } else { if ( $was_dir ) { unless (rmdir($file)) { warn "rmdir('$file') in '$dir' failed: $!\n"; next name; } } else { # if -d _ ... unless (unlink($file)) { warn "unlink('$file') in '$dir' failed: $!\n"; next name; } } } # if $TEST... } # foreach (&get_names... } # for $inode (... # Done reaping, print STDERR reapage stats printf STDERR ("All files deleted. Run time (seconds): User %d, Sys %d, Total %d, Real %d\n", (times)[0],(times)[1],(times)[0]+(times)[1],time()-$begin_reaping) unless $QUIET; } # get_names(inode); # Finds the full pathnames of all links to the given inode, and appends # all of the names in basename. sub get_names { local($inode)=@_; # The inode has a list of parents, and a separate list of names # under each parent. We must find each parent, and prepend its # name to all the names of the inode under that parent. # There are no parents of the root inode. # This is a little bogus in perl. Infinite loops are possible # here if you can rewrite the directory structure a bit, # particularly if you can change the parent/child relationships # on directories. print STDERR "get_names called on $inode (root=$root_inode)\n" if $DEBUG; return ($root_path) if ($inode==$root_inode); if (!($inode_info{$inode})) { die "WARNING: get_names called on unknown inode $inode"; } local($age,@names)=split(/\0/,$inode_info{$inode}); local(%my_names); foreach (@names) { local($parent_inode,$name)=m/^(\d+),([\000-\377]+)$/; warn "WARNING: parent_inode is null?" unless $parent_inode; foreach (&get_names($parent_inode)) { $my_names{"$_/$name"}++; } } warn "WARNING: No names found for '$inode'" unless keys(%my_names); print STDERR "get_names($inode)=".join(" ",keys(%my_names))."\n" if $DEBUG; return sort(keys(%my_names)); } # get_dir(directory,parent_inode); # Incorporates information about all files in the directory into the # global database. Returns the age of the newest file in the directory. sub get_dir { local($directory)=@_; local($newest_child_age)=0; print STDERR "get_dir($directory)\n" if $DEBUG; ($was_link,$link_target,$was_dir)=&get_inode_info($directory,"."); unless (defined($was_link)) { warn "Could not lstat '.' in '$directory': $!\n"; return; } local($parent_inode)=$ino_ino; unless (opendir(DIR,".")) { warn "Couldn't opendir '.' in '$directory': $!\n"; return; } local(@dirfiles)=readdir(DIR); closedir(DIR); foreach (@dirfiles) { next if /^\.\.?$/; # Ignore '.' and '..' ($was_link,$link_target,$was_dir)=&get_inode_info($directory,$_); next unless (defined($was_link)); if ($link_dev != $root_dev) { warn "Not crossing device in '$directory/$_' (root is $root_dev, $_ is $link_dev)\n"; # Not likely to be able to delete it, either... # I guess we could explicitly umount it... ;-) next; } local($inode)=$link_ino; local($inode_age,@parents)=split(/\0/,$inode_info{$inode}); # file, fifo, block device, char device, socket... if ($ino_atime>$ino_ctime && !$NOATIME && $ino_atime < time()+1) { $inode_age=$ino_atime; } else { $inode_age=$ino_ctime; } if ( $was_link ) { # symlink to extant file $inode_age--; # guarantee symlink goes away before file does } $inode_age+=$ROOTAGE if ( ( ! $link_uid || $MINAGE )); if ($was_link && !$NODIRS && $link_target eq '.' && $_ eq '...') { # Omit this from inode list $inode_age=time(); } else { if ( $was_dir ) { # directory $inode_age=$ino_ctime; local($subdir_age)=&get_dir("$directory/$_"); $inode_age=$subdir_age if ($subdir_age>$inode_age); } $inode_info{$inode}=join("\0",$inode_age,@parents,"$parent_inode,$_"); } $newest_child_age=$inode_age if $inode_age>$newest_child_age; #print STDERR "$inode_info{$inode}\n"; } return $newest_child_age+1; } $foo=<_HARNESS_FOR_GET_DIR; # actually for get_names too... foreach (@ARGV) { print STDERR "get_dir($_)...\n"; ($root_dev,$root_inode)=(stat($_))[0,1]; $root_path=$_; $retval=&get_dir($_); foreach (keys(%parent_of)) { print STDERR "Names of inode $_ are:\n",join("\n\t",&get_names($_)),"\n"; } } print STDERR "Exiting...\n"; exit(0); TEST_HARNESS_FOR_GET_DIR # stat_fs(percentage) # Returns number of bytes left to be freed, assuming that percentage% of # the space available to non-superuser is to be kept free. # # On some systems you have to do sync() before statfs. It's not portable: # Solaris has stat_vfs, your mileage may vary. # Linux only! Port to your own operating system! sub SYS_statfs { 99 } sub stat_fs { local($struct_statfs)="III III II2I I6"; local($param)=pack($struct_statfs,0); local($dir)="$root_path/"; if (syscall(&SYS_statfs,$dir,$param) == -1) { warn "statfs($dir) failed: $!\n"; return 0; } local($f_ftype,$f_bsize,$f_blocks,$f_bfree,$f_bavail,$f_files, $f_ffree,$f_fsid1,$f_fsid2,$f_namelen, @f_spare)=unpack($struct_statfs,$param); print STDERR "statfs: ",join(",",unpack($struct_statfs,$param)),"\n" if $DEBUG; local($root_saved)=($f_bfree-$f_bavail); local($required_user_free); if ($min_spec =~ /^(\d+)\%$/o) { $required_user_free=int(($f_blocks-$root_saved)*($1)/100); } elsif ($min_spec =~ /^(\d+)k$/io) { $required_user_free=$1; } elsif ($min_spec =~ /^(\d+)m$/io) { $required_user_free=($1)*1024; } print STDERR "root_saved=$root_saved, required_user_free=$required_user_free\n" if $DEBUG; return ($required_user_free-$f_bavail)*1024; } $foo=<_HARNESS_FOR_STAT_FS; foreach (@ARGV) { print STDERR "statfs($_)...\n"; &ch_dir($_) && warn "ch_dir error\n"; $retval=&stat_fs(); print STDERR "statfs($_) returned $retval\n"; } exit(0); TEST_HARNESS_FOR_STAT_FS # ch_dir(directory); # Changes to the directory specified, checking for symlinks. # Returns: 0 if successful # 1 if a symlink was crossed # 2 for other error # In the event of a non-success, the current directory is untrustworthy. sub ch_dir { local($directory)=@_; chdir("/") || return $CH_DIR_ERROR; local($path_so_far)=''; foreach (split(/\//,$directory)) { next unless length($_); local(@prev_dot_stat)=(lstat("."))[0,1]; unless (defined($prev_dot_stat[0])) { warn "Could not lstat '.' in '$path_so_far': $!\n"; return $CH_DIR_ERROR; } local(@prev_dir_stat)=(lstat($_))[0,1]; unless (defined($prev_dir_stat[0])) { warn "Could not lstat '$_' in '$path_so_far': $!\n"; return $CH_DIR_ERROR; } chdir($_) || return $CH_DIR_ERROR; local(@dot_dot_stat)=(lstat(".."))[0,1]; unless (defined($dot_dot_stat[0])) { warn "Could not lstat '..' in '$path_so_far/$_': $!\n"; return $CH_DIR_ERROR; } local(@dot_stat)=(lstat("."))[0,1]; return $CH_DIR_ERROR unless defined($dot_stat[0]); unless (defined($dot_stat[0])) { warn "Could not lstat '.' in '$path_so_far/$_': $!\n"; return $CH_DIR_ERROR; } # Strictly speaking, you really only need check '.' before # = '..' after. I'm paranoid... if (join(" ",@dot_stat) ne join(" ",@prev_dir_stat)) { warn "Crossed symlink entering '$path_so_far/$_': '.' != '$path_so_far/$_'\n"; return $CH_DIR_CROSS; } if (join(" ",@dot_dot_stat) ne join(" ",@prev_dot_stat)) { warn "Crossed symlink entering '$path_so_far/$_': '..' != '$path_so_far'\n"; return $CH_DIR_CROSS; } $path_so_far.="/$_"; } return $CH_DIR_SUCCESS; } $foo=<_HARNESS_FOR_CH_DIR; foreach (@ARGV) { print STDERR "ch_dir($_)..."; $retval=&ch_dir($_); print STDERR "ch_dir($_) returned $retval\n"; } exit(0); TEST_HARNESS_FOR_CH_DIR sub get_inode_info { local($dir,$name)=(@_); print STDERR "get_inode_info($dir,$name)...\n" if $DEBUG; if ($last_ch_dir ne $dir) { if (&ch_dir($dir)) { warn "ch_dir '$dir' unsuccessful: $!\n"; return undef; } else { $last_ch_dir=$dir; } } ($link_dev,$link_ino,$link_mode,$link_nlink,$link_uid,$link_gid,$link_rdev,$link_size,$link_atime, $link_mtime,$link_ctime,$link_blksize,$link_blocks)=lstat($name); $inode_was_dir=(-d _); $inode_was_link=(-l _); if ($inode_was_link) { ($ino_dev,$ino_ino,$ino_mode,$ino_nlink,$ino_uid,$ino_gid,$ino_rdev,$ino_size,$ino_atime, $ino_mtime,$ino_ctime,$ino_blksize,$ino_blocks)=stat($name); if (defined($ino_dev)) { $link_target=readlink($name); warn "Could not readlink '$name' in '$dir': $!\n" if (!defined($link_target)); } else { warn "Could not stat (yes, stat) '$name' in '$dir': $!\n"; ( $ino_dev, $ino_ino, $ino_mode, $ino_nlink, $ino_uid, $ino_gid, $ino_rdev, $ino_size, $ino_atime, $ino_mtime, $ino_ctime, $ino_blksize, $ino_blocks)= ($link_dev, $link_ino, $link_mode, $link_nlink, $link_uid, $link_gid, $link_rdev, $link_size, $link_atime, $link_mtime, $link_ctime, $link_blksize, $link_blocks); $ino_atime=$ino_ctime; $link_atime=$link_ctime; } } else { ( $ino_dev, $ino_ino, $ino_mode, $ino_nlink, $ino_uid, $ino_gid, $ino_rdev, $ino_size, $ino_atime, $ino_mtime, $ino_ctime, $ino_blksize, $ino_blocks)= ($link_dev, $link_ino, $link_mode, $link_nlink, $link_uid, $link_gid, $link_rdev, $link_size, $link_atime, $link_mtime, $link_ctime, $link_blksize, $link_blocks); } if (!defined($ino_dev)) { warn "Could not lstat '$name' in '$dir': $!\n"; return undef; } return $inode_was_link,$link_target,$inode_was_dir; } #