#!/bin/sh #- # Copyright 2004-2007 Colin Percival # All rights reserved # # Redistribution and use in source and binary forms, with or without # modification, are permitted providing that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # $FreeBSD: stable/11/usr.sbin/freebsd-update/freebsd-update.sh 302539 2016-07-11 04:50:32Z delphij $ #### Usage function -- called from command-line handling code. # Usage instructions. Options not listed: # --debug -- don't filter output from utilities # --no-stats -- don't show progress statistics while fetching files proc usage { cat << """ usage: $[basename $0] [options] command ... [path] Options: -b basedir -- Operate on a system mounted at basedir (default: /) -d workdir -- Store working files in workdir (default: /var/db/freebsd-update/) -f conffile -- Read configuration options from conffile (default: /etc/freebsd-update.conf) -F -- Force a fetch operation to proceed -k KEY -- Trust an RSA key with SHA256 hash of KEY -r release -- Target for upgrade (e.g., 11.1-RELEASE) -s server -- Server from which to fetch updates (default: update.FreeBSD.org) -t address -- Mail output of cron command, if any, to address (default: root) --not-running-from-cron -- Run without a tty, for use by automated tools --currently-running release -- Update as if currently running this release Commands: fetch -- Fetch updates from server cron -- Sleep rand(3600) seconds, fetch updates, and send an email if updates were found upgrade -- Fetch upgrades to FreeBSD version specified via -r option install -- Install downloaded updates or upgrades rollback -- Uninstall most recently installed updates IDS -- Compare the system against an index of "known good" files. """ exit 0 } #### Configuration processing functions #- # Configuration options are set in the following order of priority: # 1. Command line options # 2. Configuration file options # 3. Default options # In addition, certain options (e.g., IgnorePaths) can be specified multiple # times and (as long as these are all in the same place, e.g., inside the # configuration file) they will accumulate. Finally, because the path to the # configuration file can be specified at the command line, the entire command # line must be processed before we start reading the configuration file. # # Sound like a mess? It is. Here's how we handle this: # 1. Initialize CONFFILE and all the options to "". # 2. Process the command line. Throw an error if a non-accumulating option # is specified twice. # 3. If CONFFILE is "", set CONFFILE to /etc/freebsd-update.conf . # 4. For all the configuration options X, set X_saved to X. # 5. Initialize all the options to "". # 6. Read CONFFILE line by line, parsing options. # 7. For each configuration option X, set X to X_saved iff X_saved is not "". # 8. Repeat steps 4-7, except setting options to their default values at (6). setglobal CONFIGOPTIONS = '"KEYPRINT WORKDIR SERVERNAME MAILTO ALLOWADD ALLOWDELETE KEEPMODIFIEDMETADATA COMPONENTS IGNOREPATHS UPDATEIFUNMODIFIED BASEDIR VERBOSELEVEL TARGETRELEASE STRICTCOMPONENTS MERGECHANGES IDSIGNOREPATHS BACKUPKERNEL BACKUPKERNELDIR BACKUPKERNELSYMBOLFILES'" # Set all the configuration options to "". proc nullconfig { for X in [$(CONFIGOPTIONS)] { eval $(X)="" } } # For each configuration option X, set X_saved to X. proc saveconfig { for X in [$(CONFIGOPTIONS)] { eval $(X)_saved='$'$(X) } } # For each configuration option X, set X to X_saved if X_saved is not "". proc mergeconfig { for X in [$(CONFIGOPTIONS)] { eval _='$'$(X)_saved if ! test -z $(_) { eval $(X)='$'$(X)_saved } } } # Set the trusted keyprint. proc config_KeyPrint { if test -z $(KEYPRINT) { setglobal KEYPRINT = $1 } else { return 1 } } # Set the working directory. proc config_WorkDir { if test -z $(WORKDIR) { setglobal WORKDIR = $1 } else { return 1 } } # Set the name of the server (pool) from which to fetch updates proc config_ServerName { if test -z $(SERVERNAME) { setglobal SERVERNAME = $1 } else { return 1 } } # Set the address to which 'cron' output will be mailed. proc config_MailTo { if test -z $(MAILTO) { setglobal MAILTO = $1 } else { return 1 } } # Set whether FreeBSD Update is allowed to add files (or directories, or # symlinks) which did not previously exist. proc config_AllowAdd { if test -z $(ALLOWADD) { match $1 { with [Yy][Ee][Ss] setglobal ALLOWADD = 'yes' with [Nn][Oo] setglobal ALLOWADD = 'no' with * return 1 } } else { return 1 } } # Set whether FreeBSD Update is allowed to remove files/directories/symlinks. proc config_AllowDelete { if test -z $(ALLOWDELETE) { match $1 { with [Yy][Ee][Ss] setglobal ALLOWDELETE = 'yes' with [Nn][Oo] setglobal ALLOWDELETE = 'no' with * return 1 } } else { return 1 } } # Set whether FreeBSD Update should keep existing inode ownership, # permissions, and flags, in the event that they have been modified locally # after the release. proc config_KeepModifiedMetadata { if test -z $(KEEPMODIFIEDMETADATA) { match $1 { with [Yy][Ee][Ss] setglobal KEEPMODIFIEDMETADATA = 'yes' with [Nn][Oo] setglobal KEEPMODIFIEDMETADATA = 'no' with * return 1 } } else { return 1 } } # Add to the list of components which should be kept updated. proc config_Components { for C in [$ifsjoin(Argv)] { if test $C = "src" { if test -e /usr/src/COPYRIGHT { setglobal COMPONENTS = ""$(COMPONENTS) $(C)"" } else { echo "src component not installed, skipped" } } else { setglobal COMPONENTS = ""$(COMPONENTS) $(C)"" } } } # Add to the list of paths under which updates will be ignored. proc config_IgnorePaths { for C in [$ifsjoin(Argv)] { setglobal IGNOREPATHS = ""$(IGNOREPATHS) $(C)"" } } # Add to the list of paths which IDS should ignore. proc config_IDSIgnorePaths { for C in [$ifsjoin(Argv)] { setglobal IDSIGNOREPATHS = ""$(IDSIGNOREPATHS) $(C)"" } } # Add to the list of paths within which updates will be performed only if the # file on disk has not been modified locally. proc config_UpdateIfUnmodified { for C in [$ifsjoin(Argv)] { setglobal UPDATEIFUNMODIFIED = ""$(UPDATEIFUNMODIFIED) $(C)"" } } # Add to the list of paths within which updates to text files will be merged # instead of overwritten. proc config_MergeChanges { for C in [$ifsjoin(Argv)] { setglobal MERGECHANGES = ""$(MERGECHANGES) $(C)"" } } # Work on a FreeBSD installation mounted under $1 proc config_BaseDir { if test -z $(BASEDIR) { setglobal BASEDIR = $1 } else { return 1 } } # When fetching upgrades, should we assume the user wants exactly the # components listed in COMPONENTS, rather than trying to guess based on # what's currently installed? proc config_StrictComponents { if test -z $(STRICTCOMPONENTS) { match $1 { with [Yy][Ee][Ss] setglobal STRICTCOMPONENTS = 'yes' with [Nn][Oo] setglobal STRICTCOMPONENTS = 'no' with * return 1 } } else { return 1 } } # Upgrade to FreeBSD $1 proc config_TargetRelease { if test -z $(TARGETRELEASE) { setglobal TARGETRELEASE = $1 } else { return 1 } if echo $(TARGETRELEASE) | grep -qE '^[0-9.]+$' { setglobal TARGETRELEASE = ""$(TARGETRELEASE)-RELEASE"" } } # Define what happens to output of utilities proc config_VerboseLevel { if test -z $(VERBOSELEVEL) { match $1 { with [Dd][Ee][Bb][Uu][Gg] setglobal VERBOSELEVEL = 'debug' with [Nn][Oo][Ss][Tt][Aa][Tt][Ss] setglobal VERBOSELEVEL = 'nostats' with [Ss][Tt][Aa][Tt][Ss] setglobal VERBOSELEVEL = 'stats' with * return 1 } } else { return 1 } } proc config_BackupKernel { if test -z $(BACKUPKERNEL) { match $1 { with [Yy][Ee][Ss] setglobal BACKUPKERNEL = 'yes' with [Nn][Oo] setglobal BACKUPKERNEL = 'no' with * return 1 } } else { return 1 } } proc config_BackupKernelDir { if test -z $(BACKUPKERNELDIR) { if test -z $1 { echo "BackupKernelDir set to empty dir" return 1 } # We check for some paths which would be extremely odd # to use, but which could cause a lot of problems if # used. match $1 { with /|/bin|/boot|/etc|/lib|/libexec|/sbin|/usr|/var echo "BackupKernelDir set to invalid path $1" return 1 with /* setglobal BACKUPKERNELDIR = $1 with * echo "BackupKernelDir ($1) is not an absolute path" return 1 } } else { return 1 } } proc config_BackupKernelSymbolFiles { if test -z $(BACKUPKERNELSYMBOLFILES) { match $1 { with [Yy][Ee][Ss] setglobal BACKUPKERNELSYMBOLFILES = 'yes' with [Nn][Oo] setglobal BACKUPKERNELSYMBOLFILES = 'no' with * return 1 } } else { return 1 } } # Handle one line of configuration proc configline { if test $Argc -eq 0 { return } setglobal OPT = $1 shift config_$(OPT) $ifsjoin(Argv) } #### Parameter handling functions. # Initialize parameters to null, just in case they're # set in the environment. proc init_params { # Configration settings nullconfig # No configuration file set yet setglobal CONFFILE = ''"" # No commands specified yet setglobal COMMANDS = ''"" # Force fetch to proceed setglobal FORCEFETCH = '0' # Run without a TTY setglobal NOTTYOK = '0' } # Parse the command line proc parse_cmdline { while test $Argc -gt 0 { match $1 { # Location of configuration file with -f if test $Argc -eq 1 { usage; } if test ! -z $(CONFFILE) { usage; } shift; setglobal CONFFILE = $1 with -F setglobal FORCEFETCH = '1' with --not-running-from-cron setglobal NOTTYOK = '1' with --currently-running shift; export UNAME_r="$1" # Configuration file equivalents with -b if test $Argc -eq 1 { usage; }; shift config_BaseDir $1 || usage with -d if test $Argc -eq 1 { usage; }; shift config_WorkDir $1 || usage with -k if test $Argc -eq 1 { usage; }; shift config_KeyPrint $1 || usage with -s if test $Argc -eq 1 { usage; }; shift config_ServerName $1 || usage with -r if test $Argc -eq 1 { usage; }; shift config_TargetRelease $1 || usage with -t if test $Argc -eq 1 { usage; }; shift config_MailTo $1 || usage with -v if test $Argc -eq 1 { usage; }; shift config_VerboseLevel $1 || usage # Aliases for "-v debug" and "-v nostats" with --debug config_VerboseLevel debug || usage with --no-stats config_VerboseLevel nostats || usage # Commands with cron | fetch | upgrade | install | rollback | IDS setglobal COMMANDS = ""$(COMMANDS) $1"" # Anything else is an error with * usage } shift } # Make sure we have at least one command if test -z $(COMMANDS) { usage } } # Parse the configuration file proc parse_conffile { # If a configuration file was specified on the command line, check # that it exists and is readable. if test ! -z $(CONFFILE) && test ! -r $(CONFFILE) { echo -n "File does not exist " echo -n "or is not readable: " echo $(CONFFILE) exit 1 } # If a configuration file was not specified on the command line, # use the default configuration file path. If that default does # not exist, give up looking for any configuration. if test -z $(CONFFILE) { setglobal CONFFILE = '"/etc/freebsd-update.conf'" if test ! -r $(CONFFILE) { return } } # Save the configuration options specified on the command line, and # clear all the options in preparation for reading the config file. saveconfig nullconfig # Read the configuration file. Anything after the first '#' is # ignored, and any blank lines are ignored. setglobal L = '0' while read LINE { setglobal L = $shExpr('$L + 1') setglobal LINEX = $[echo $(LINE) | cut -f 1 -d '#] if ! configline $(LINEX) { echo "Error processing configuration file, line $L:" echo "==> $(LINE)" exit 1 } } < ${CONFFILE} # Merge the settings read from the configuration file with those # provided at the command line. mergeconfig } # Provide some default parameters proc default_params { # Save any parameters already configured, and clear the slate saveconfig nullconfig # Default configurations config_WorkDir /var/db/freebsd-update config_MailTo root config_AllowAdd yes config_AllowDelete yes config_KeepModifiedMetadata yes config_BaseDir / config_VerboseLevel stats config_StrictComponents no config_BackupKernel yes config_BackupKernelDir /boot/kernel.old config_BackupKernelSymbolFiles no # Merge these defaults into the earlier-configured settings mergeconfig } # Set utility output filtering options, based on ${VERBOSELEVEL} proc fetch_setup_verboselevel { match $(VERBOSELEVEL) { with debug setglobal QUIETREDIR = '"/dev/stderr'" setglobal QUIETFLAG = '" '" setglobal STATSREDIR = '"/dev/stderr'" setglobal DDSTATS = '"..'" setglobal XARGST = '"-t'" setglobal NDEBUG = '" '" with nostats setglobal QUIETREDIR = ''"" setglobal QUIETFLAG = ''"" setglobal STATSREDIR = '"/dev/null'" setglobal DDSTATS = '"..'" setglobal XARGST = ''"" setglobal NDEBUG = ''"" with stats setglobal QUIETREDIR = '"/dev/null'" setglobal QUIETFLAG = '"-q'" setglobal STATSREDIR = '"/dev/stdout'" setglobal DDSTATS = ''"" setglobal XARGST = ''"" setglobal NDEBUG = '"-n'" } } # Perform sanity checks and set some final parameters # in preparation for fetching files. Figure out which # set of updates should be downloaded: If the user is # running *-p[0-9]+, strip off the last part; if the # user is running -SECURITY, call it -RELEASE. Chdir # into the working directory. proc fetchupgrade_check_params { export HTTP_USER_AGENT="freebsd-update ($(COMMAND), $[uname -r])" setglobal _SERVERNAME_z = '\ "SERVERNAME must be given via command line or configuration file.'" setglobal _KEYPRINT_z = '"Key must be given via -k option or configuration file.'" setglobal _KEYPRINT_bad = '"Invalid key fingerprint: '" setglobal _WORKDIR_bad = '"Directory does not exist or is not writable: '" setglobal _WORKDIR_bad2 = '"Directory is not on a persistent filesystem: '" if test -z $(SERVERNAME) { echo -n "$[basename $0]: " echo $(_SERVERNAME_z) exit 1 } if test -z $(KEYPRINT) { echo -n "$[basename $0]: " echo $(_KEYPRINT_z) exit 1 } if ! echo $(KEYPRINT) | grep -qE "^[0-9a-f]{64}$" { echo -n "$[basename $0]: " echo -n $(_KEYPRINT_bad) echo $(KEYPRINT) exit 1 } if ! test -d $(WORKDIR) -a -w $(WORKDIR) { echo -n "$[basename $0]: " echo -n $(_WORKDIR_bad) echo $(WORKDIR) exit 1 } match $[df -T $(WORKDIR)] { with */dev/md[0-9]* | *tmpfs* echo -n "$[basename $0]: " echo -n $(_WORKDIR_bad2) echo $(WORKDIR) exit 1 } chmod 700 $(WORKDIR) cd $(WORKDIR) || exit 1 # Generate release number. The s/SECURITY/RELEASE/ bit exists # to provide an upgrade path for FreeBSD Update 1.x users, since # the kernels provided by FreeBSD Update 1.x are always labelled # as X.Y-SECURITY. setglobal RELNUM = $[uname -r | sed -E 's,-p[0-9]+,,' | sed -E 's,-SECURITY,-RELEASE,] setglobal ARCH = $[uname -m] setglobal FETCHDIR = "$(RELNUM)/$(ARCH)" setglobal PATCHDIR = "$(RELNUM)/$(ARCH)/bp" # Figure out what directory contains the running kernel setglobal BOOTFILE = $[sysctl -n kern.bootfile] setglobal KERNELDIR = $(BOOTFILE%/kernel) if ! test -d $(KERNELDIR) { echo "Cannot identify running kernel" exit 1 } # Figure out what kernel configuration is running. We start with # the output of `uname -i`, and then make the following adjustments: # 1. Replace "SMP-GENERIC" with "SMP". Why the SMP kernel config # file says "ident SMP-GENERIC", I don't know... # 2. If the kernel claims to be GENERIC _and_ ${ARCH} is "amd64" # _and_ `sysctl kern.version` contains a line which ends "/SMP", then # we're running an SMP kernel. This mis-identification is a bug # which was fixed in 6.2-STABLE. setglobal KERNCONF = $[uname -i] if test $(KERNCONF) = "SMP-GENERIC" { setglobal KERNCONF = 'SMP' } if test $(KERNCONF) = "GENERIC" && test $(ARCH) = "amd64" { if sysctl kern.version | grep -qE '/SMP$' { setglobal KERNCONF = 'SMP' } } # Define some paths setglobal BSPATCH = '/usr/bin/bspatch' setglobal SHA256 = '/sbin/sha256' setglobal PHTTPGET = '/usr/libexec/phttpget' # Set up variables relating to VERBOSELEVEL fetch_setup_verboselevel # Construct a unique name from ${BASEDIR} setglobal BDHASH = $[echo $(BASEDIR) | sha256 -q] } # Perform sanity checks etc. before fetching updates. proc fetch_check_params { fetchupgrade_check_params if ! test -z $(TARGETRELEASE) { echo -n "$[basename $0]: " echo -n "-r option is meaningless with 'fetch' command. " echo "(Did you mean 'upgrade' instead?)" exit 1 } # Check that we have updates ready to install if test -f $(BDHASH)-install/kerneldone -a $FORCEFETCH -eq 0 { echo "You have a partially completed upgrade pending" echo "Run '$0 install' first." echo "Run '$0 fetch -F' to proceed anyway." exit 1 } } # Perform sanity checks etc. before fetching upgrades. proc upgrade_check_params { fetchupgrade_check_params # Unless set otherwise, we're upgrading to the same kernel config. setglobal NKERNCONF = $(KERNCONF) # We need TARGETRELEASE set setglobal _TARGETRELEASE_z = '"Release target must be specified via -r option.'" if test -z $(TARGETRELEASE) { echo -n "$[basename $0]: " echo $(_TARGETRELEASE_z) exit 1 } # The target release should be != the current release. if test $(TARGETRELEASE) = $(RELNUM) { echo -n "$[basename $0]: " echo "Cannot upgrade from $(RELNUM) to itself" exit 1 } # Turning off AllowAdd or AllowDelete is a bad idea for upgrades. if test $(ALLOWADD) = "no" { echo -n "$[basename $0]: " echo -n "WARNING: \"AllowAdd no\" is a bad idea " echo "when upgrading between releases." echo } if test $(ALLOWDELETE) = "no" { echo -n "$[basename $0]: " echo -n "WARNING: \"AllowDelete no\" is a bad idea " echo "when upgrading between releases." echo } # Set EDITOR to /usr/bin/vi if it isn't already set : $(EDITOR:='/usr/bin/vi') } # Perform sanity checks and set some final parameters in # preparation for installing updates. proc install_check_params { # Check that we are root. All sorts of things won't work otherwise. if test $[id -u] != 0 { echo "You must be root to run this." exit 1 } # Check that securelevel <= 0. Otherwise we can't update schg files. if test $[sysctl -n kern.securelevel] -gt 0 { echo "Updates cannot be installed when the system securelevel" echo "is greater than zero." exit 1 } # Check that we have a working directory setglobal _WORKDIR_bad = '"Directory does not exist or is not writable: '" if ! test -d $(WORKDIR) -a -w $(WORKDIR) { echo -n "$[basename $0]: " echo -n $(_WORKDIR_bad) echo $(WORKDIR) exit 1 } cd $(WORKDIR) || exit 1 # Construct a unique name from ${BASEDIR} setglobal BDHASH = $[echo $(BASEDIR) | sha256 -q] # Check that we have updates ready to install if ! test -L $(BDHASH)-install { echo "No updates are available to install." echo "Run '$0 fetch' first." exit 1 } if ! test -f $(BDHASH)-install/INDEX-OLD || ! test -f $(BDHASH)-install/INDEX-NEW { echo "Update manifest is corrupt -- this should never happen." echo "Re-run '$0 fetch'." exit 1 } # Figure out what directory contains the running kernel setglobal BOOTFILE = $[sysctl -n kern.bootfile] setglobal KERNELDIR = $(BOOTFILE%/kernel) if ! test -d $(KERNELDIR) { echo "Cannot identify running kernel" exit 1 } } # Perform sanity checks and set some final parameters in # preparation for UNinstalling updates. proc rollback_check_params { # Check that we are root. All sorts of things won't work otherwise. if test $[id -u] != 0 { echo "You must be root to run this." exit 1 } # Check that we have a working directory setglobal _WORKDIR_bad = '"Directory does not exist or is not writable: '" if ! test -d $(WORKDIR) -a -w $(WORKDIR) { echo -n "$[basename $0]: " echo -n $(_WORKDIR_bad) echo $(WORKDIR) exit 1 } cd $(WORKDIR) || exit 1 # Construct a unique name from ${BASEDIR} setglobal BDHASH = $[echo $(BASEDIR) | sha256 -q] # Check that we have updates ready to rollback if ! test -L $(BDHASH)-rollback { echo "No rollback directory found." exit 1 } if ! test -f $(BDHASH)-rollback/INDEX-OLD || ! test -f $(BDHASH)-rollback/INDEX-NEW { echo "Update manifest is corrupt -- this should never happen." exit 1 } } # Perform sanity checks and set some final parameters # in preparation for comparing the system against the # published index. Figure out which index we should # compare against: If the user is running *-p[0-9]+, # strip off the last part; if the user is running # -SECURITY, call it -RELEASE. Chdir into the working # directory. proc IDS_check_params { export HTTP_USER_AGENT="freebsd-update ($(COMMAND), $[uname -r])" setglobal _SERVERNAME_z = '\ "SERVERNAME must be given via command line or configuration file.'" setglobal _KEYPRINT_z = '"Key must be given via -k option or configuration file.'" setglobal _KEYPRINT_bad = '"Invalid key fingerprint: '" setglobal _WORKDIR_bad = '"Directory does not exist or is not writable: '" if test -z $(SERVERNAME) { echo -n "$[basename $0]: " echo $(_SERVERNAME_z) exit 1 } if test -z $(KEYPRINT) { echo -n "$[basename $0]: " echo $(_KEYPRINT_z) exit 1 } if ! echo $(KEYPRINT) | grep -qE "^[0-9a-f]{64}$" { echo -n "$[basename $0]: " echo -n $(_KEYPRINT_bad) echo $(KEYPRINT) exit 1 } if ! test -d $(WORKDIR) -a -w $(WORKDIR) { echo -n "$[basename $0]: " echo -n $(_WORKDIR_bad) echo $(WORKDIR) exit 1 } cd $(WORKDIR) || exit 1 # Generate release number. The s/SECURITY/RELEASE/ bit exists # to provide an upgrade path for FreeBSD Update 1.x users, since # the kernels provided by FreeBSD Update 1.x are always labelled # as X.Y-SECURITY. setglobal RELNUM = $[uname -r | sed -E 's,-p[0-9]+,,' | sed -E 's,-SECURITY,-RELEASE,] setglobal ARCH = $[uname -m] setglobal FETCHDIR = "$(RELNUM)/$(ARCH)" setglobal PATCHDIR = "$(RELNUM)/$(ARCH)/bp" # Figure out what directory contains the running kernel setglobal BOOTFILE = $[sysctl -n kern.bootfile] setglobal KERNELDIR = $(BOOTFILE%/kernel) if ! test -d $(KERNELDIR) { echo "Cannot identify running kernel" exit 1 } # Figure out what kernel configuration is running. We start with # the output of `uname -i`, and then make the following adjustments: # 1. Replace "SMP-GENERIC" with "SMP". Why the SMP kernel config # file says "ident SMP-GENERIC", I don't know... # 2. If the kernel claims to be GENERIC _and_ ${ARCH} is "amd64" # _and_ `sysctl kern.version` contains a line which ends "/SMP", then # we're running an SMP kernel. This mis-identification is a bug # which was fixed in 6.2-STABLE. setglobal KERNCONF = $[uname -i] if test $(KERNCONF) = "SMP-GENERIC" { setglobal KERNCONF = 'SMP' } if test $(KERNCONF) = "GENERIC" && test $(ARCH) = "amd64" { if sysctl kern.version | grep -qE '/SMP$' { setglobal KERNCONF = 'SMP' } } # Define some paths setglobal SHA256 = '/sbin/sha256' setglobal PHTTPGET = '/usr/libexec/phttpget' # Set up variables relating to VERBOSELEVEL fetch_setup_verboselevel } #### Core functionality -- the actual work gets done here # Use an SRV query to pick a server. If the SRV query doesn't provide # a useful answer, use the server name specified by the user. # Put another way... look up _http._tcp.${SERVERNAME} and pick a server # from that; or if no servers are returned, use ${SERVERNAME}. # This allows a user to specify "portsnap.freebsd.org" (in which case # portsnap will select one of the mirrors) or "portsnap5.tld.freebsd.org" # (in which case portsnap will use that particular server, since there # won't be an SRV entry for that name). # # We ignore the Port field, since we are always going to use port 80. # Fetch the mirror list, but do not pick a mirror yet. Returns 1 if # no mirrors are available for any reason. proc fetch_pick_server_init { : > serverlist_tried # Check that host(1) exists (i.e., that the system wasn't built with the # WITHOUT_BIND set) and don't try to find a mirror if it doesn't exist. if ! which -s host { : > serverlist_full return 1 } echo -n "Looking up $(SERVERNAME) mirrors... " # Issue the SRV query and pull out the Priority, Weight, and Target fields. # BIND 9 prints "$name has SRV record ..." while BIND 8 prints # "$name server selection ..."; we allow either format. setglobal MLIST = ""_http._tcp.$(SERVERNAME)"" host -t srv $(MLIST) | sed -nE "s/$(MLIST) (has SRV record|server selection) //p" | cut -f 1,2,4 -d ' ' | sed -e 's/\.$//' | sort > serverlist_full # If no records, give up -- we'll just use the server name we were given. if test $[wc -l < serverlist_full] -eq 0 { echo "none found." return 1 } # Report how many mirrors we found. echo $[wc -l < serverlist_full] "mirrors found." # Generate a random seed for use in picking mirrors. If HTTP_PROXY # is set, this will be used to generate the seed; otherwise, the seed # will be random. if test -n "$(HTTP_PROXY)$(http_proxy)" { setglobal RANDVALUE = $[sha256 -qs "$(HTTP_PROXY)$(http_proxy)" | tr -d 'a-f' | cut -c 1-9] } else { setglobal RANDVALUE = $[jot -r 1 0 999999999] } } # Pick a mirror. Returns 1 if we have run out of mirrors to try. proc fetch_pick_server { # Generate a list of not-yet-tried mirrors sort serverlist_tried | comm -23 serverlist_full - > serverlist # Have we run out of mirrors? if test $[wc -l < serverlist] -eq 0 { echo "No mirrors remaining, giving up." return 1 } # Find the highest priority level (lowest numeric value). setglobal SRV_PRIORITY = $[cut -f 1 -d ' ' serverlist | sort -n | head -1] # Add up the weights of the response lines at that priority level. setglobal SRV_WSUM = '0'; while read X { match $X { with ${SRV_PRIORITY}\ * setglobal SRV_W = $[echo $X | cut -f 2 -d ' ] setglobal SRV_WSUM = $shExpr('$SRV_WSUM + $SRV_W') } } < serverlist # If all the weights are 0, pretend that they are all 1 instead. if test $(SRV_WSUM) -eq 0 { setglobal SRV_WSUM = $[grep -E "^$(SRV_PRIORITY) " serverlist | wc -l] setglobal SRV_W_ADD = '1' } else { setglobal SRV_W_ADD = '0' } # Pick a value between 0 and the sum of the weights - 1 setglobal SRV_RND = $[expr $(RANDVALUE) % $(SRV_WSUM)] # Read through the list of mirrors and set SERVERNAME. Write the line # corresponding to the mirror we selected into serverlist_tried so that # we won't try it again. while read X { match $X { with ${SRV_PRIORITY}\ * setglobal SRV_W = $[echo $X | cut -f 2 -d ' ] setglobal SRV_W = $shExpr('$SRV_W + $SRV_W_ADD') if test $SRV_RND -lt $SRV_W { setglobal SERVERNAME = $[echo $X | cut -f 3 -d ' ] echo $X >> serverlist_tried break } else { setglobal SRV_RND = $shExpr('$SRV_RND - $SRV_W') } } } < serverlist } # Take a list of ${oldhash}|${newhash} and output a list of needed patches, # i.e., those for which we have ${oldhash} and don't have ${newhash}. proc fetch_make_patchlist { grep -vE "^([0-9a-f]{64})\|\1$" | tr '|' ' ' | while read X Y { if test -f "files/$(Y).gz" || test ! -f "files/$(X).gz" { continue } echo "$(X)|$(Y)" } | uniq } # Print user-friendly progress statistics proc fetch_progress { setglobal LNC = '0' while read x { setglobal LNC = $shExpr('$LNC + 1') if test $shExpr('$LNC % 10') = 0 { echo -n $LNC } elif test $shExpr('$LNC % 2') = 0 { echo -n . } } echo -n " " } # Function for asking the user if everything is ok proc continuep { while read -p "Does this look reasonable (y/n)? " CONTINUE { match $(CONTINUE) { with y* return 0 with n* return 1 } } } # Initialize the working directory proc workdir_init { mkdir -p files touch tINDEX.present } # Check that we have a public key with an appropriate hash, or # fetch the key if it doesn't exist. Returns 1 if the key has # not yet been fetched. proc fetch_key { if test -r pub.ssl && test $[$(SHA256) -q pub.ssl] = $(KEYPRINT) { return 0 } echo -n "Fetching public key from $(SERVERNAME)... " rm -f pub.ssl fetch $(QUIETFLAG) http://$(SERVERNAME)/$(FETCHDIR)/pub.ssl \ !2 >$(QUIETREDIR) || true if ! test -r pub.ssl { echo "failed." return 1 } if ! test $[$(SHA256) -q pub.ssl] = $(KEYPRINT) { echo "key has incorrect hash." rm -f pub.ssl return 1 } echo "done." } # Fetch metadata signature, aka "tag". proc fetch_tag { echo -n "Fetching metadata signature " echo $(NDEBUG) "for $(RELNUM) from $(SERVERNAME)... " rm -f latest.ssl fetch $(QUIETFLAG) http://$(SERVERNAME)/$(FETCHDIR)/latest.ssl \ !2 >$(QUIETREDIR) || true if ! test -r latest.ssl { echo "failed." return 1 } openssl rsautl -pubin -inkey pub.ssl -verify \ < latest.ssl > tag.new !2 >$(QUIETREDIR) || true rm latest.ssl if ! test $[wc -l < tag.new] = 1 || ! grep -qE \ "^freebsd-update\|$(ARCH)\|$(RELNUM)\|[0-9]+\|[0-9a-f]{64}\|[0-9]{10}" \ tag.new { echo "invalid signature." return 1 } echo "done." setglobal RELPATCHNUM = $[cut -f 4 -d '|' < tag.new] setglobal TINDEXHASH = $[cut -f 5 -d '|' < tag.new] setglobal EOLTIME = $[cut -f 6 -d '|' < tag.new] } # Sanity-check the patch number in a tag, to make sure that we're not # going to "update" backwards and to prevent replay attacks. proc fetch_tagsanity { # Check that we're not going to move from -pX to -pY with Y < X. setglobal RELPX = $[uname -r | sed -E 's,.*-,,] if echo $(RELPX) | grep -qE '^p[0-9]+$' { setglobal RELPX = $[echo $(RELPX) | cut -c 2-] } else { setglobal RELPX = '0' } if test $(RELPATCHNUM) -lt $(RELPX) { echo echo -n "Files on mirror ($(RELNUM)-p$(RELPATCHNUM))" echo " appear older than what" echo "we are currently running ($[uname -r])!" echo "Cowardly refusing to proceed any further." return 1 } # If "tag" exists and corresponds to ${RELNUM}, make sure that # it contains a patch number <= RELPATCHNUM, in order to protect # against rollback (replay) attacks. if test -f tag && grep -qE \ "^freebsd-update\|$(ARCH)\|$(RELNUM)\|[0-9]+\|[0-9a-f]{64}\|[0-9]{10}" \ tag { setglobal LASTRELPATCHNUM = $[cut -f 4 -d '|' < tag] if test $(RELPATCHNUM) -lt $(LASTRELPATCHNUM) { echo echo -n "Files on mirror ($(RELNUM)-p$(RELPATCHNUM))" echo " are older than the" echo -n "most recently seen updates" echo " ($(RELNUM)-p$(LASTRELPATCHNUM))." echo "Cowardly refusing to proceed any further." return 1 } } } # Fetch metadata index file proc fetch_metadata_index { echo $(NDEBUG) "Fetching metadata index... " rm -f $(TINDEXHASH) fetch $(QUIETFLAG) http://$(SERVERNAME)/$(FETCHDIR)/t/$(TINDEXHASH) !2 >$(QUIETREDIR) if ! test -f $(TINDEXHASH) { echo "failed." return 1 } if test $[$(SHA256) -q $(TINDEXHASH)] != $(TINDEXHASH) { echo "update metadata index corrupt." return 1 } echo "done." } # Print an error message about signed metadata being bogus. proc fetch_metadata_bogus { echo echo "The update metadata$1 is correctly signed, but" echo "failed an integrity check." echo "Cowardly refusing to proceed any further." return 1 } # Construct tINDEX.new by merging the lines named in $1 from ${TINDEXHASH} # with the lines not named in $@ from tINDEX.present (if that file exists). proc fetch_metadata_index_merge { for METAFILE in [$ifsjoin(Argv)] { if test $[grep -E "^$(METAFILE)\|" $(TINDEXHASH) | wc -l] \ -ne 1 { fetch_metadata_bogus " index" return 1 } grep -E "$(METAFILE)\|" $(TINDEXHASH) } | sort > tINDEX.wanted if test -f tINDEX.present { join -t '|' -v 2 tINDEX.wanted tINDEX.present | sort -m - tINDEX.wanted > tINDEX.new rm tINDEX.wanted } else { mv tINDEX.wanted tINDEX.new } } # Sanity check all the lines of tINDEX.new. Even if more metadata lines # are added by future versions of the server, this won't cause problems, # since the only lines which appear in tINDEX.new are the ones which we # specifically grepped out of ${TINDEXHASH}. proc fetch_metadata_index_sanity { if grep -qvE '^[0-9A-Z.-]+\|[0-9a-f]{64}$' tINDEX.new { fetch_metadata_bogus " index" return 1 } } # Sanity check the metadata file $1. proc fetch_metadata_sanity { # Some aliases to save space later: ${P} is a character which can # appear in a path; ${M} is the four numeric metadata fields; and # ${H} is a sha256 hash. setglobal P = '"[-+./:=,%@_[~[:alnum:]]'" setglobal M = '"[0-9]+\|[0-9]+\|[0-9]+\|[0-9]+'" setglobal H = '"[0-9a-f]{64}'" # Check that the first four fields make sense. if gunzip -c < files/$1.gz | grep -qvE "^[a-z]+\|[0-9a-z-]+\|$(P)+\|[fdL-]\|" { fetch_metadata_bogus "" return 1 } # Remove the first three fields. gunzip -c < files/$1.gz | cut -f 4- -d '|' > sanitycheck.tmp # Sanity check entries with type 'f' if grep -E '^f' sanitycheck.tmp | grep -qvE "^f\|$(M)\|$(H)\|$(P)*\$" { fetch_metadata_bogus "" return 1 } # Sanity check entries with type 'd' if grep -E '^d' sanitycheck.tmp | grep -qvE "^d\|$(M)\|\|\$" { fetch_metadata_bogus "" return 1 } # Sanity check entries with type 'L' if grep -E '^L' sanitycheck.tmp | grep -qvE "^L\|$(M)\|$(P)*\|\$" { fetch_metadata_bogus "" return 1 } # Sanity check entries with type '-' if grep -E '^-' sanitycheck.tmp | grep -qvE "^-\|\|\|\|\|\|" { fetch_metadata_bogus "" return 1 } # Clean up rm sanitycheck.tmp } # Fetch the metadata index and metadata files listed in $@, # taking advantage of metadata patches where possible. proc fetch_metadata { fetch_metadata_index || return 1 fetch_metadata_index_merge $ifsjoin(Argv) || return 1 fetch_metadata_index_sanity || return 1 # Generate a list of wanted metadata patches join -t '|' -o 1.2,2.2 tINDEX.present tINDEX.new | fetch_make_patchlist > patchlist if test -s patchlist { # Attempt to fetch metadata patches echo -n "Fetching $[wc -l < patchlist | tr -d ' ] " echo $(NDEBUG) "metadata patches.$(DDSTATS)" tr '|' '-' < patchlist | lam -s "$(FETCHDIR)/tp/" - -s ".gz" | xargs $(XARGST) $(PHTTPGET) $(SERVERNAME) \ !2 >$(STATSREDIR) | fetch_progress echo "done." # Attempt to apply metadata patches echo -n "Applying metadata patches... " tr '|' ' ' < patchlist | while read X Y { if test ! -f "$(X)-$(Y).gz" { continue; } gunzip -c < $(X)-$(Y).gz > diff gunzip -c < files/$(X).gz > diff-OLD # Figure out which lines are being added and removed grep -E '^-' diff | cut -c 2- | while read PREFIX { look $(PREFIX) diff-OLD } | sort > diff-rm grep -E '^\+' diff | cut -c 2- > diff-add # Generate the new file comm -23 diff-OLD diff-rm | sort - diff-add > diff-NEW if test $[$(SHA256) -q diff-NEW] = $(Y) { mv diff-NEW files/$(Y) gzip -n files/$(Y) } else { mv diff-NEW $(Y).bad } rm -f $(X)-$(Y).gz diff rm -f diff-OLD diff-NEW diff-add diff-rm } 2>${QUIETREDIR} echo "done." } # Update metadata without patches cut -f 2 -d '|' < tINDEX.new | while read Y { if test ! -f "files/$(Y).gz" { echo $(Y); } } | sort -u > filelist if test -s filelist { echo -n "Fetching $[wc -l < filelist | tr -d ' ] " echo $(NDEBUG) "metadata files... " lam -s "$(FETCHDIR)/m/" - -s ".gz" < filelist | xargs $(XARGST) $(PHTTPGET) $(SERVERNAME) \ !2 >$(QUIETREDIR) while read Y { if ! test -f $(Y).gz { echo "failed." return 1 } if test $[gunzip -c < $(Y).gz | $(SHA256) -q] = $(Y) { mv $(Y).gz files/$(Y).gz } else { echo "metadata is corrupt." return 1 } } < filelist echo "done." } # Sanity-check the metadata files. cut -f 2 -d '|' tINDEX.new > filelist while read X { fetch_metadata_sanity $(X) || return 1 } < filelist # Remove files which are no longer needed cut -f 2 -d '|' tINDEX.present | sort > oldfiles cut -f 2 -d '|' tINDEX.new | sort | comm -13 - oldfiles | lam -s "files/" - -s ".gz" | xargs rm -f rm patchlist filelist oldfiles rm $(TINDEXHASH) # We're done! mv tINDEX.new tINDEX.present mv tag.new tag return 0 } # Extract a subset of a downloaded metadata file containing only the parts # which are listed in COMPONENTS. proc fetch_filter_metadata_components { setglobal METAHASH = $[look "$1|" tINDEX.present | cut -f 2 -d '|] gunzip -c < files/$(METAHASH).gz > $1.all # Fish out the lines belonging to components we care about. for C in [$(COMPONENTS)] { look "$[echo $(C) | tr '/' '|]|" $1.all } > $1 # Remove temporary file. rm $1.all } # Generate a filtered version of the metadata file $1 from the downloaded # file, by fishing out the lines corresponding to components we're trying # to keep updated, and then removing lines corresponding to paths we want # to ignore. proc fetch_filter_metadata { # Fish out the lines belonging to components we care about. fetch_filter_metadata_components $1 # Canonicalize directory names by removing any trailing / in # order to avoid listing directories multiple times if they # belong to multiple components. Turning "/" into "" doesn't # matter, since we add a leading "/" when we use paths later. cut -f 3- -d '|' $1 | sed -e 's,/|d|,|d|,' | sed -e 's,/|-|,|-|,' | sort -u > $1.tmp # Figure out which lines to ignore and remove them. for X in [$(IGNOREPATHS)] { grep -E "^$(X)" $1.tmp } | sort -u | comm -13 - $1.tmp > $1 # Remove temporary files. rm $1.tmp } # Filter the metadata file $1 by adding lines with "/boot/$2" # replaced by ${KERNELDIR} (which is `sysctl -n kern.bootfile` minus the # trailing "/kernel"); and if "/boot/$2" does not exist, remove # the original lines which start with that. # Put another way: Deal with the fact that the FOO kernel is sometimes # installed in /boot/FOO/ and is sometimes installed elsewhere. proc fetch_filter_kernel_names { grep ^/boot/$2 $1 | sed -e "s,/boot/$2,$(KERNELDIR),g" | sort - $1 > $1.tmp mv $1.tmp $1 if ! test -d /boot/$2 { grep -v ^/boot/$2 $1 > $1.tmp mv $1.tmp $1 } } # For all paths appearing in $1 or $3, inspect the system # and generate $2 describing what is currently installed. proc fetch_inspect_system { # No errors yet... rm -f .err # Tell the user why his disk is suddenly making lots of noise echo -n "Inspecting system... " # Generate list of files to inspect cat $1 $3 | cut -f 1 -d '|' | sort -u > filelist # Examine each file and output lines of the form # /path/to/file|type|device-inum|user|group|perm|flags|value # sorted by device and inode number. while read F { # If the symlink/file/directory does not exist, record this. if ! test -e $(BASEDIR)/$(F) { echo "$(F)|-||||||" continue } if ! test -r $(BASEDIR)/$(F) { echo "Cannot read file: $(BASEDIR)/$(F)" \ >/dev/stderr touch .err return 1 } # Otherwise, output an index line. if test -L $(BASEDIR)/$(F) { echo -n "$(F)|L|" stat -n -f '%d-%i|%u|%g|%Mp%Lp|%Of|' $(BASEDIR)/$(F); readlink $(BASEDIR)/$(F); } elif test -f $(BASEDIR)/$(F) { echo -n "$(F)|f|" stat -n -f '%d-%i|%u|%g|%Mp%Lp|%Of|' $(BASEDIR)/$(F); sha256 -q $(BASEDIR)/$(F); } elif test -d $(BASEDIR)/$(F) { echo -n "$(F)|d|" stat -f '%d-%i|%u|%g|%Mp%Lp|%Of|' $(BASEDIR)/$(F); } else { echo "Unknown file type: $(BASEDIR)/$(F)" \ >/dev/stderr touch .err return 1 } } < filelist | sort -k 3,3 -t '|' > $2.tmp rm filelist # Check if an error occurred during system inspection if test -f .err { return 1 } # Convert to the form # /path/to/file|type|user|group|perm|flags|value|hlink # by resolving identical device and inode numbers into hard links. cut -f 1,3 -d '|' $2.tmp | sort -k 1,1 -t '|' | sort -s -u -k 2,2 -t '|' | join -1 2 -2 3 -t '|' - $2.tmp | awk -F '|' -v OFS='|' \ '{ if (($2 == $3) || ($4 == "-")) print $3,$4,$5,$6,$7,$8,$9,"" else print $3,$4,$5,$6,$7,$8,$9,$2 }' | sort > $2 rm $2.tmp # We're finished looking around echo "done." } # For any paths matching ${MERGECHANGES}, compare $1 and $2 and find any # files which differ; generate $3 containing these paths and the old hashes. proc fetch_filter_mergechanges { # Pull out the paths and hashes of the files matching ${MERGECHANGES}. for F in [$1 $2] { for X in [$(MERGECHANGES)] { grep -E "^$(X)" $(F) } | cut -f 1,2,7 -d '|' | sort > $(F)-values } # Any line in $2-values which doesn't appear in $1-values and is a # file means that we should list the path in $3. comm -13 $1-values $2-values | fgrep '|f|' | cut -f 1 -d '|' > $2-paths # For each path, pull out one (and only one!) entry from $1-values. # Note that we cannot distinguish which "old" version the user made # changes to; but hopefully any changes which occur due to security # updates will exist in both the "new" version and the version which # the user has installed, so the merging will still work. while read X { look "$(X)|" $1-values | head -1 } < $2-paths > $3 # Clean up rm $1-values $2-values $2-paths } # For any paths matching ${UPDATEIFUNMODIFIED}, remove lines from $[123] # which correspond to lines in $2 with hashes not matching $1 or $3, unless # the paths are listed in $4. For entries in $2 marked "not present" # (aka. type -), remove lines from $[123] unless there is a corresponding # entry in $1. proc fetch_filter_unmodified_notpresent { # Figure out which lines of $1 and $3 correspond to bits which # should only be updated if they haven't changed, and fish out # the (path, type, value) tuples. # NOTE: We don't consider a file to be "modified" if it matches # the hash from $3. for X in [$(UPDATEIFUNMODIFIED)] { grep -E "^$(X)" $1 grep -E "^$(X)" $3 } | cut -f 1,2,7 -d '|' | sort > $1-values # Do the same for $2. for X in [$(UPDATEIFUNMODIFIED)] { grep -E "^$(X)" $2 } | cut -f 1,2,7 -d '|' | sort > $2-values # Any entry in $2-values which is not in $1-values corresponds to # a path which we need to remove from $1, $2, and $3, unless it # that path appears in $4. comm -13 $1-values $2-values | sort -t '|' -k 1,1 > mlines.tmp cut -f 1 -d '|' $4 | sort | join -v 2 -t '|' - mlines.tmp | sort > mlines rm $1-values $2-values mlines.tmp # Any lines in $2 which are not in $1 AND are "not present" lines # also belong in mlines. comm -13 $1 $2 | cut -f 1,2,7 -d '|' | fgrep '|-|' >> mlines # Remove lines from $1, $2, and $3 for X in [$1 $2 $3] { sort -t '|' -k 1,1 $(X) > $(X).tmp cut -f 1 -d '|' < mlines | sort | join -v 2 -t '|' - $(X).tmp | sort > $(X) rm $(X).tmp } # Store a list of the modified files, for future reference fgrep -v '|-|' mlines | cut -f 1 -d '|' > modifiedfiles rm mlines } # For each entry in $1 of type -, remove any corresponding # entry from $2 if ${ALLOWADD} != "yes". Remove all entries # of type - from $1. proc fetch_filter_allowadd { cut -f 1,2 -d '|' < $1 | fgrep '|-' | cut -f 1 -d '|' > filesnotpresent if test $(ALLOWADD) != "yes" { sort < $2 | join -v 1 -t '|' - filesnotpresent | sort > $2.tmp mv $2.tmp $2 } sort < $1 | join -v 1 -t '|' - filesnotpresent | sort > $1.tmp mv $1.tmp $1 rm filesnotpresent } # If ${ALLOWDELETE} != "yes", then remove any entries from $1 # which don't correspond to entries in $2. proc fetch_filter_allowdelete { # Produce a lists ${PATH}|${TYPE} for X in [$1 $2] { cut -f 1-2 -d '|' < $(X) | sort -u > $(X).nodes } # Figure out which lines need to be removed from $1. if test $(ALLOWDELETE) != "yes" { comm -23 $1.nodes $2.nodes > $1.badnodes } else { : > $1.badnodes } # Remove the relevant lines from $1 while read X { look "$(X)|" $1 } < $1.badnodes | comm -13 - $1 > $1.tmp mv $1.tmp $1 rm $1.badnodes $1.nodes $2.nodes } # If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in $2 # with metadata not matching any entry in $1, replace the corresponding # line of $3 with one having the same metadata as the entry in $2. proc fetch_filter_modified_metadata { # Fish out the metadata from $1 and $2 for X in [$1 $2] { cut -f 1-6 -d '|' < $(X) > $(X).metadata } # Find the metadata we need to keep if test $(KEEPMODIFIEDMETADATA) = "yes" { comm -13 $1.metadata $2.metadata > keepmeta } else { : > keepmeta } # Extract the lines which we need to remove from $3, and # construct the lines which we need to add to $3. : > $3.remove : > $3.add while read LINE { setglobal NODE = $[echo $(LINE) | cut -f 1-2 -d '|] look "$(NODE)|" $3 >> $3.remove look "$(NODE)|" $3 | cut -f 7- -d '|' | lam -s "$(LINE)|" - >> $3.add } < keepmeta # Remove the specified lines and add the new lines. sort $3.remove | comm -13 - $3 | sort -u - $3.add > $3.tmp mv $3.tmp $3 rm keepmeta $1.metadata $2.metadata $3.add $3.remove } # Remove lines from $1 and $2 which are identical; # no need to update a file if it isn't changing. proc fetch_filter_uptodate { comm -23 $1 $2 > $1.tmp comm -13 $1 $2 > $2.tmp mv $1.tmp $1 mv $2.tmp $2 } # Fetch any "clean" old versions of files we need for merging changes. proc fetch_files_premerge { # We only need to do anything if $1 is non-empty. if test -s $1 { # Tell the user what we're doing echo -n "Fetching files from $(OLDRELNUM) for merging... " # List of files wanted fgrep '|f|' < $1 | cut -f 3 -d '|' | sort -u > files.wanted # Only fetch the files we don't already have while read Y { if test ! -f "files/$(Y).gz" { echo $(Y); } } < files.wanted > filelist # Actually fetch them lam -s "$(OLDFETCHDIR)/f/" - -s ".gz" < filelist | xargs $(XARGST) $(PHTTPGET) $(SERVERNAME) \ !2 >$(QUIETREDIR) # Make sure we got them all, and move them into /files/ while read Y { if ! test -f $(Y).gz { echo "failed." return 1 } if test $[gunzip -c < $(Y).gz | $(SHA256) -q] = $(Y) { mv $(Y).gz files/$(Y).gz } else { echo "$(Y) has incorrect hash." return 1 } } < filelist echo "done." # Clean up rm filelist files.wanted } } # Prepare to fetch files: Generate a list of the files we need, # copy the unmodified files we have into /files/, and generate # a list of patches to download. proc fetch_files_prepare { # Tell the user why his disk is suddenly making lots of noise echo -n "Preparing to download files... " # Reduce indices to ${PATH}|${HASH} pairs for X in [$1 $2 $3] { cut -f 1,2,7 -d '|' < $(X) | fgrep '|f|' | cut -f 1,3 -d '|' | sort > $(X).hashes } # List of files wanted cut -f 2 -d '|' < $3.hashes | sort -u | while read HASH { if ! test -f files/$(HASH).gz { echo $(HASH) } } > files.wanted # Generate a list of unmodified files comm -12 $1.hashes $2.hashes | sort -k 1,1 -t '|' > unmodified.files # Copy all files into /files/. We only need the unmodified files # for use in patching; but we'll want all of them if the user asks # to rollback the updates later. while read LINE { setglobal F = $[echo $(LINE) | cut -f 1 -d '|] setglobal HASH = $[echo $(LINE) | cut -f 2 -d '|] # Skip files we already have. if test -f files/$(HASH).gz { continue } # Make sure the file hasn't changed. cp "$(BASEDIR)/$(F)" tmpfile if test $[sha256 -q tmpfile] != $(HASH) { echo echo "File changed while FreeBSD Update running: $(F)" return 1 } # Place the file into storage. gzip -c < tmpfile > files/$(HASH).gz rm tmpfile } < $2.hashes # Produce a list of patches to download sort -k 1,1 -t '|' $3.hashes | join -t '|' -o 2.2,1.2 - unmodified.files | fetch_make_patchlist > patchlist # Garbage collect rm unmodified.files $1.hashes $2.hashes $3.hashes # We don't need the list of possible old files any more. rm $1 # We're finished making noise echo "done." } # Fetch files. proc fetch_files { # Attempt to fetch patches if test -s patchlist { echo -n "Fetching $[wc -l < patchlist | tr -d ' ] " echo $(NDEBUG) "patches.$(DDSTATS)" tr '|' '-' < patchlist | lam -s "$(PATCHDIR)/" - | xargs $(XARGST) $(PHTTPGET) $(SERVERNAME) \ !2 >$(STATSREDIR) | fetch_progress echo "done." # Attempt to apply patches echo -n "Applying patches... " tr '|' ' ' < patchlist | while read X Y { if test ! -f "$(X)-$(Y)" { continue; } gunzip -c < files/$(X).gz > OLD bspatch OLD NEW $(X)-$(Y) if test $[$(SHA256) -q NEW] = $(Y) { mv NEW files/$(Y) gzip -n files/$(Y) } rm -f diff OLD NEW $(X)-$(Y) } 2>${QUIETREDIR} echo "done." } # Download files which couldn't be generate via patching while read Y { if test ! -f "files/$(Y).gz" { echo $(Y); } } < files.wanted > filelist if test -s filelist { echo -n "Fetching $[wc -l < filelist | tr -d ' ] " echo $(NDEBUG) "files... " lam -s "$(FETCHDIR)/f/" - -s ".gz" < filelist | xargs $(XARGST) $(PHTTPGET) $(SERVERNAME) \ !2 >$(QUIETREDIR) while read Y { if ! test -f $(Y).gz { echo "failed." return 1 } if test $[gunzip -c < $(Y).gz | $(SHA256) -q] = $(Y) { mv $(Y).gz files/$(Y).gz } else { echo "$(Y) has incorrect hash." return 1 } } < filelist echo "done." } # Clean up rm files.wanted filelist patchlist } # Create and populate install manifest directory; and report what updates # are available. proc fetch_create_manifest { # If we have an existing install manifest, nuke it. if test -L "$(BDHASH)-install" { rm -r $(BDHASH)-install/ rm $(BDHASH)-install } # Report to the user if any updates were avoided due to local changes if test -s modifiedfiles { echo echo -n "The following files are affected by updates, " echo "but no changes have" echo -n "been downloaded because the files have been " echo "modified locally:" cat modifiedfiles } | $PAGER rm modifiedfiles # If no files will be updated, tell the user and exit if ! test -s INDEX-PRESENT && ! test -s INDEX-NEW { rm INDEX-PRESENT INDEX-NEW echo echo -n "No updates needed to update system to " echo "$(RELNUM)-p$(RELPATCHNUM)." return } # Divide files into (a) removed files, (b) added files, and # (c) updated files. cut -f 1 -d '|' < INDEX-PRESENT | sort > INDEX-PRESENT.flist cut -f 1 -d '|' < INDEX-NEW | sort > INDEX-NEW.flist comm -23 INDEX-PRESENT.flist INDEX-NEW.flist > files.removed comm -13 INDEX-PRESENT.flist INDEX-NEW.flist > files.added comm -12 INDEX-PRESENT.flist INDEX-NEW.flist > files.updated rm INDEX-PRESENT.flist INDEX-NEW.flist # Report removed files, if any if test -s files.removed { echo echo -n "The following files will be removed " echo "as part of updating to $(RELNUM)-p$(RELPATCHNUM):" cat files.removed } | $PAGER rm files.removed # Report added files, if any if test -s files.added { echo echo -n "The following files will be added " echo "as part of updating to $(RELNUM)-p$(RELPATCHNUM):" cat files.added } | $PAGER rm files.added # Report updated files, if any if test -s files.updated { echo echo -n "The following files will be updated " echo "as part of updating to $(RELNUM)-p$(RELPATCHNUM):" cat files.updated } | $PAGER rm files.updated # Create a directory for the install manifest. setglobal MDIR = $[mktemp -d install.XXXXXX] || return 1 # Populate it mv INDEX-PRESENT $(MDIR)/INDEX-OLD mv INDEX-NEW $(MDIR)/INDEX-NEW # Link it into place ln -s $(MDIR) $(BDHASH)-install } # Warn about any upcoming EoL proc fetch_warn_eol { # What's the current time? setglobal NOWTIME = $[date "+%s] # When did we last warn about the EoL date? if test -f lasteolwarn { setglobal LASTWARN = $[cat lasteolwarn] } else { setglobal LASTWARN = $[expr $(NOWTIME) - 63072000] } # If the EoL time is past, warn. if test $(EOLTIME) -lt $(NOWTIME) { echo cat << """ WARNING: $[uname -sr] HAS PASSED ITS END-OF-LIFE DATE. Any security issues discovered after $[date -r $(EOLTIME)] will not have been corrected. """ return 1 } # Figure out how long it has been since we last warned about the # upcoming EoL, and how much longer we have left. setglobal SINCEWARN = $[expr $(NOWTIME) - $(LASTWARN)] setglobal TIMELEFT = $[expr $(EOLTIME) - $(NOWTIME)] # Don't warn if the EoL is more than 3 months away if test $(TIMELEFT) -gt 7884000 { return 0 } # Don't warn if the time remaining is more than 3 times the time # since the last warning. if test $(TIMELEFT) -gt $[expr $(SINCEWARN) '*' 3] { return 0 } # Figure out what time units to use. if test $(TIMELEFT) -lt 604800 { setglobal UNIT = '"day'" setglobal SIZE = '86400' } elif test $(TIMELEFT) -lt 2678400 { setglobal UNIT = '"week'" setglobal SIZE = '604800' } else { setglobal UNIT = '"month'" setglobal SIZE = '2678400' } # Compute the right number of units setglobal NUM = $[expr $(TIMELEFT) / $(SIZE)] if test $(NUM) != 1 { setglobal UNIT = ""$(UNIT)s"" } # Print the warning echo cat << """ WARNING: $[uname -sr] is approaching its End-of-Life date. It is strongly recommended that you upgrade to a newer release within the next $(NUM) $(UNIT). """ # Update the stored time of last warning echo $(NOWTIME) > lasteolwarn } # Do the actual work involved in "fetch" / "cron". proc fetch_run { workdir_init || return 1 # Prepare the mirror list. fetch_pick_server_init && fetch_pick_server # Try to fetch the public key until we run out of servers. while ! fetch_key { fetch_pick_server || return 1 } # Try to fetch the metadata index signature ("tag") until we run # out of available servers; and sanity check the downloaded tag. while ! fetch_tag { fetch_pick_server || return 1 } fetch_tagsanity || return 1 # Fetch the latest INDEX-NEW and INDEX-OLD files. fetch_metadata INDEX-NEW INDEX-OLD || return 1 # Generate filtered INDEX-NEW and INDEX-OLD files containing only # the lines which (a) belong to components we care about, and (b) # don't correspond to paths we're explicitly ignoring. fetch_filter_metadata INDEX-NEW || return 1 fetch_filter_metadata INDEX-OLD || return 1 # Translate /boot/${KERNCONF} into ${KERNELDIR} fetch_filter_kernel_names INDEX-NEW $(KERNCONF) fetch_filter_kernel_names INDEX-OLD $(KERNCONF) # For all paths appearing in INDEX-OLD or INDEX-NEW, inspect the # system and generate an INDEX-PRESENT file. fetch_inspect_system INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1 # Based on ${UPDATEIFUNMODIFIED}, remove lines from INDEX-* which # correspond to lines in INDEX-PRESENT with hashes not appearing # in INDEX-OLD or INDEX-NEW. Also remove lines where the entry in # INDEX-PRESENT has type - and there isn't a corresponding entry in # INDEX-OLD with type -. fetch_filter_unmodified_notpresent \ INDEX-OLD INDEX-PRESENT INDEX-NEW /dev/null # For each entry in INDEX-PRESENT of type -, remove any corresponding # entry from INDEX-NEW if ${ALLOWADD} != "yes". Remove all entries # of type - from INDEX-PRESENT. fetch_filter_allowadd INDEX-PRESENT INDEX-NEW # If ${ALLOWDELETE} != "yes", then remove any entries from # INDEX-PRESENT which don't correspond to entries in INDEX-NEW. fetch_filter_allowdelete INDEX-PRESENT INDEX-NEW # If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in # INDEX-PRESENT with metadata not matching any entry in INDEX-OLD, # replace the corresponding line of INDEX-NEW with one having the # same metadata as the entry in INDEX-PRESENT. fetch_filter_modified_metadata INDEX-OLD INDEX-PRESENT INDEX-NEW # Remove lines from INDEX-PRESENT and INDEX-NEW which are identical; # no need to update a file if it isn't changing. fetch_filter_uptodate INDEX-PRESENT INDEX-NEW # Prepare to fetch files: Generate a list of the files we need, # copy the unmodified files we have into /files/, and generate # a list of patches to download. fetch_files_prepare INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1 # Fetch files. fetch_files || return 1 # Create and populate install manifest directory; and report what # updates are available. fetch_create_manifest || return 1 # Warn about any upcoming EoL fetch_warn_eol || return 1 } # If StrictComponents is not "yes", generate a new components list # with only the components which appear to be installed. proc upgrade_guess_components { if test $(STRICTCOMPONENTS) = "no" { # Generate filtered INDEX-ALL with only the components listed # in COMPONENTS. fetch_filter_metadata_components $1 || return 1 # Tell the user why his disk is suddenly making lots of noise echo -n "Inspecting system... " # Look at the files on disk, and assume that a component is # supposed to be present if it is more than half-present. cut -f 1-3 -d '|' < INDEX-ALL | tr '|' ' ' | while read C S F { if test -e $(BASEDIR)/$(F) { echo "+ $(C)|$(S)" } echo "= $(C)|$(S)" } | sort | uniq -c | sed -E 's,^ +,,' > compfreq grep ' = ' compfreq | cut -f 1,3 -d ' ' | sort -k 2,2 -t ' ' > compfreq.total grep ' + ' compfreq | cut -f 1,3 -d ' ' | sort -k 2,2 -t ' ' > compfreq.present join -t ' ' -1 2 -2 2 compfreq.present compfreq.total | while read S P T { if test $(P) -gt $[expr $(T) / 2] { echo $(S) } } > comp.present cut -f 2 -d ' ' < compfreq.total > comp.total rm INDEX-ALL compfreq compfreq.total compfreq.present # We're done making noise. echo "done." # Sometimes the kernel isn't installed where INDEX-ALL # thinks that it should be: In particular, it is often in # /boot/kernel instead of /boot/GENERIC or /boot/SMP. To # deal with this, if "kernel|X" is listed in comp.total # (i.e., is a component which would be upgraded if it is # found to be present) we will add it to comp.present. # If "kernel|" is in comp.total but "kernel|X" is # not, we print a warning -- the user is running a kernel # which isn't part of the release. setglobal KCOMP = $[echo $(KERNCONF) | tr 'A-Z' 'a-z] grep -E "^kernel\|$(KCOMP)\$" comp.total >> comp.present if grep -qE "^kernel\|" comp.total && ! grep -qE "^kernel\|$(KCOMP)\$" comp.total { cat << """ WARNING: This system is running a "$(KCOMP)" kernel, which is not a kernel configuration distributed as part of FreeBSD $(RELNUM). This kernel will not be updated: you MUST update the kernel manually before running "$0 install". """ } # Re-sort the list of installed components and generate # the list of non-installed components. sort -u < comp.present > comp.present.tmp mv comp.present.tmp comp.present comm -13 comp.present comp.total > comp.absent # Ask the user to confirm that what we have is correct. To # reduce user confusion, translate "X|Y" back to "X/Y" (as # subcomponents must be listed in the configuration file). echo echo -n "The following components of FreeBSD " echo "seem to be installed:" tr '|' '/' < comp.present | fmt -72 echo echo -n "The following components of FreeBSD " echo "do not seem to be installed:" tr '|' '/' < comp.absent | fmt -72 echo continuep || return 1 echo # Suck the generated list of components into ${COMPONENTS}. # Note that comp.present.tmp is used due to issues with # pipelines and setting variables. setglobal COMPONENTS = ''"" tr '|' '/' < comp.present > comp.present.tmp while read C { setglobal COMPONENTS = ""$(COMPONENTS) $(C)"" } < comp.present.tmp # Delete temporary files rm comp.present comp.present.tmp comp.absent comp.total } } # If StrictComponents is not "yes", COMPONENTS contains an entry # corresponding to the currently running kernel, and said kernel # does not exist in the new release, add "kernel/generic" to the # list of components. proc upgrade_guess_new_kernel { if test $(STRICTCOMPONENTS) = "no" { # Grab the unfiltered metadata file. setglobal METAHASH = $[look "$1|" tINDEX.present | cut -f 2 -d '|] gunzip -c < files/$(METAHASH).gz > $1.all # If "kernel/${KCOMP}" is in ${COMPONENTS} and that component # isn't in $1.all, we need to add kernel/generic. for C in [$(COMPONENTS)] { if test $(C) = "kernel/$(KCOMP)" && ! grep -qE "^kernel\|$(KCOMP)\|" $1.all { setglobal COMPONENTS = ""$(COMPONENTS) kernel/generic"" setglobal NKERNCONF = '"GENERIC'" cat << """ WARNING: This system is running a "$(KCOMP)" kernel, which is not a kernel configuration distributed as part of FreeBSD $(RELNUM). As part of upgrading to FreeBSD $(RELNUM), this kernel will be replaced with a "generic" kernel. """ continuep || return 1 } } # Don't need this any more... rm $1.all } } # Convert INDEX-OLD (last release) and INDEX-ALL (new release) into # INDEX-OLD and INDEX-NEW files (in the sense of normal upgrades). proc upgrade_oldall_to_oldnew { # For each ${F}|... which appears in INDEX-ALL but does not appear # in INDEX-OLD, add ${F}|-|||||| to INDEX-OLD. cut -f 1 -d '|' < $1 | sort -u > $1.paths cut -f 1 -d '|' < $2 | sort -u | comm -13 $1.paths - | lam - -s "|-||||||" | sort - $1 > $1.tmp mv $1.tmp $1 # Remove lines from INDEX-OLD which also appear in INDEX-ALL comm -23 $1 $2 > $1.tmp mv $1.tmp $1 # Remove lines from INDEX-ALL which have a file name not appearing # anywhere in INDEX-OLD (since these must be files which haven't # changed -- if they were new, there would be an entry of type "-"). cut -f 1 -d '|' < $1 | sort -u > $1.paths sort -k 1,1 -t '|' < $2 | join -t '|' - $1.paths | sort > $2.tmp rm $1.paths mv $2.tmp $2 # Rename INDEX-ALL to INDEX-NEW. mv $2 $3 } # Helper for upgrade_merge: Return zero true iff the two files differ only # in the contents of their RCS tags. proc samef { setglobal X = $[sed -E 's/\\$FreeBSD.*\\$/\$FreeBSD\$/' < $1 | $(SHA256)] setglobal Y = $[sed -E 's/\\$FreeBSD.*\\$/\$FreeBSD\$/' < $2 | $(SHA256)] if test $X = $Y { return 0; } else { return 1; } } # From the list of "old" files in $1, merge changes in $2 with those in $3, # and update $3 to reflect the hashes of merged files. proc upgrade_merge { # We only need to do anything if $1 is non-empty. if test -s $1 { cut -f 1 -d '|' $1 | sort > $1-paths # Create staging area for merging files rm -rf merge/ while read F { setglobal D = $[dirname $(F)] mkdir -p merge/old/$(D) mkdir -p merge/$(OLDRELNUM)/$(D) mkdir -p merge/$(RELNUM)/$(D) mkdir -p merge/new/$(D) } < $1-paths # Copy in files while read F { # Currently installed file setglobal V = $[look "$(F)|" $2 | cut -f 7 -d '|] gunzip < files/$(V).gz > merge/old/$(F) # Old release if look "$(F)|" $1 | fgrep -q "|f|" { setglobal V = $[look "$(F)|" $1 | cut -f 3 -d '|] gunzip < files/$(V).gz \ > merge/$(OLDRELNUM)/$(F) } # New release if look "$(F)|" $3 | cut -f 1,2,7 -d '|' | fgrep -q "|f|" { setglobal V = $[look "$(F)|" $3 | cut -f 7 -d '|] gunzip < files/$(V).gz \ > merge/$(RELNUM)/$(F) } } < $1-paths # Attempt to automatically merge changes echo -n "Attempting to automatically merge " echo -n "changes in files..." : > failed.merges while read F { # If the file doesn't exist in the new release, # the result of "merging changes" is having the file # not exist. if ! test -f merge/$(RELNUM)/$(F) { continue } # If the file didn't exist in the old release, we're # going to throw away the existing file and hope that # the version from the new release is what we want. if ! test -f merge/$(OLDRELNUM)/$(F) { cp merge/$(RELNUM)/$(F) merge/new/$(F) continue } # Some files need special treatment. match $(F) { with /etc/spwd.db | /etc/pwd.db | /etc/login.conf.db # Don't merge these -- we're rebuild them # after updates are installed. cp merge/old/$(F) merge/new/$(F) with * if ! merge -p -L "current version" \ -L $(OLDRELNUM) -L $(RELNUM) \ merge/old/$(F) \ merge/$(OLDRELNUM)/$(F) \ merge/$(RELNUM)/$(F) \ > merge/new/$(F) !2 >/dev/null { echo $(F) >> failed.merges } } } < $1-paths echo " done." # Ask the user to handle any files which didn't merge. while read F { # If the installed file differs from the version in # the old release only due to RCS tag expansion # then just use the version in the new release. if samef merge/old/$(F) merge/$(OLDRELNUM)/$(F) { cp merge/$(RELNUM)/$(F) merge/new/$(F) continue } cat << """ The following file could not be merged automatically: $(F) Press Enter to edit this file in $(EDITOR) and resolve the conflicts manually... """ read dummy files/$(V).gz echo "$(F)|$(V)" } } < $1-paths > newhashes # Pull lines out from $3 which need to be updated to # reflect merged files. while read F { look "$(F)|" $3 } < $1-paths > $3-oldlines # Update lines to reflect merged files join -t '|' -o 1.1,1.2,1.3,1.4,1.5,1.6,2.2,1.8 \ $3-oldlines newhashes > $3-newlines # Remove old lines from $3 and add new lines. sort $3-oldlines | comm -13 - $3 | sort - $3-newlines > $3.tmp mv $3.tmp $3 # Clean up rm $1-paths newhashes $3-oldlines $3-newlines rm -rf merge/ } # We're done with merging files. rm $1 } # Do the work involved in fetching upgrades to a new release proc upgrade_run { workdir_init || return 1 # Prepare the mirror list. fetch_pick_server_init && fetch_pick_server # Try to fetch the public key until we run out of servers. while ! fetch_key { fetch_pick_server || return 1 } # Try to fetch the metadata index signature ("tag") until we run # out of available servers; and sanity check the downloaded tag. while ! fetch_tag { fetch_pick_server || return 1 } fetch_tagsanity || return 1 # Fetch the INDEX-OLD and INDEX-ALL. fetch_metadata INDEX-OLD INDEX-ALL || return 1 # If StrictComponents is not "yes", generate a new components list # with only the components which appear to be installed. upgrade_guess_components INDEX-ALL || return 1 # Generate filtered INDEX-OLD and INDEX-ALL files containing only # the components we want and without anything marked as "Ignore". fetch_filter_metadata INDEX-OLD || return 1 fetch_filter_metadata INDEX-ALL || return 1 # Merge the INDEX-OLD and INDEX-ALL files into INDEX-OLD. sort INDEX-OLD INDEX-ALL > INDEX-OLD.tmp mv INDEX-OLD.tmp INDEX-OLD rm INDEX-ALL # Adjust variables for fetching files from the new release. setglobal OLDRELNUM = $(RELNUM) setglobal RELNUM = $(TARGETRELEASE) setglobal OLDFETCHDIR = $(FETCHDIR) setglobal FETCHDIR = "$(RELNUM)/$(ARCH)" # Try to fetch the NEW metadata index signature ("tag") until we run # out of available servers; and sanity check the downloaded tag. while ! fetch_tag { fetch_pick_server || return 1 } # Fetch the new INDEX-ALL. fetch_metadata INDEX-ALL || return 1 # If StrictComponents is not "yes", COMPONENTS contains an entry # corresponding to the currently running kernel, and said kernel # does not exist in the new release, add "kernel/generic" to the # list of components. upgrade_guess_new_kernel INDEX-ALL || return 1 # Filter INDEX-ALL to contain only the components we want and without # anything marked as "Ignore". fetch_filter_metadata INDEX-ALL || return 1 # Convert INDEX-OLD (last release) and INDEX-ALL (new release) into # INDEX-OLD and INDEX-NEW files (in the sense of normal upgrades). upgrade_oldall_to_oldnew INDEX-OLD INDEX-ALL INDEX-NEW # Translate /boot/${KERNCONF} or /boot/${NKERNCONF} into ${KERNELDIR} fetch_filter_kernel_names INDEX-NEW $(NKERNCONF) fetch_filter_kernel_names INDEX-OLD $(KERNCONF) # For all paths appearing in INDEX-OLD or INDEX-NEW, inspect the # system and generate an INDEX-PRESENT file. fetch_inspect_system INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1 # Based on ${MERGECHANGES}, generate a file tomerge-old with the # paths and hashes of old versions of files to merge. fetch_filter_mergechanges INDEX-OLD INDEX-PRESENT tomerge-old # Based on ${UPDATEIFUNMODIFIED}, remove lines from INDEX-* which # correspond to lines in INDEX-PRESENT with hashes not appearing # in INDEX-OLD or INDEX-NEW. Also remove lines where the entry in # INDEX-PRESENT has type - and there isn't a corresponding entry in # INDEX-OLD with type -. fetch_filter_unmodified_notpresent \ INDEX-OLD INDEX-PRESENT INDEX-NEW tomerge-old # For each entry in INDEX-PRESENT of type -, remove any corresponding # entry from INDEX-NEW if ${ALLOWADD} != "yes". Remove all entries # of type - from INDEX-PRESENT. fetch_filter_allowadd INDEX-PRESENT INDEX-NEW # If ${ALLOWDELETE} != "yes", then remove any entries from # INDEX-PRESENT which don't correspond to entries in INDEX-NEW. fetch_filter_allowdelete INDEX-PRESENT INDEX-NEW # If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in # INDEX-PRESENT with metadata not matching any entry in INDEX-OLD, # replace the corresponding line of INDEX-NEW with one having the # same metadata as the entry in INDEX-PRESENT. fetch_filter_modified_metadata INDEX-OLD INDEX-PRESENT INDEX-NEW # Remove lines from INDEX-PRESENT and INDEX-NEW which are identical; # no need to update a file if it isn't changing. fetch_filter_uptodate INDEX-PRESENT INDEX-NEW # Fetch "clean" files from the old release for merging changes. fetch_files_premerge tomerge-old # Prepare to fetch files: Generate a list of the files we need, # copy the unmodified files we have into /files/, and generate # a list of patches to download. fetch_files_prepare INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1 # Fetch patches from to-${RELNUM}/${ARCH}/bp/ setglobal PATCHDIR = "to-$(RELNUM)/$(ARCH)/bp" fetch_files || return 1 # Merge configuration file changes. upgrade_merge tomerge-old INDEX-PRESENT INDEX-NEW || return 1 # Create and populate install manifest directory; and report what # updates are available. fetch_create_manifest || return 1 # Leave a note behind to tell the "install" command that the kernel # needs to be installed before the world. touch $(BDHASH)-install/kernelfirst # Remind the user that they need to run "freebsd-update install" # to install the downloaded bits, in case they didn't RTFM. echo "To install the downloaded upgrades, run \"$0 install\"." } # Make sure that all the file hashes mentioned in $@ have corresponding # gzipped files stored in /files/. proc install_verify { # Generate a list of hashes cat $ifsjoin(Argv) | cut -f 2,7 -d '|' | grep -E '^f' | cut -f 2 -d '|' | sort -u > filelist # Make sure all the hashes exist while read HASH { if ! test -f files/$(HASH).gz { echo -n "Update files missing -- " echo "this should never happen." echo "Re-run '$0 fetch'." return 1 } } < filelist # Clean up rm filelist } # Remove the system immutable flag from files proc install_unschg { # Generate file list cat $ifsjoin(Argv) | cut -f 1 -d '|' > filelist # Remove flags while read F { if ! test -e $(BASEDIR)/$(F) { continue } else { echo $(BASEDIR)/$(F) } } < filelist | xargs chflags noschg || return 1 # Clean up rm filelist } # Decide which directory name to use for kernel backups. proc backup_kernel_finddir { setglobal CNT = '0' while true { # Pathname does not exist, so it is OK use that name # for backup directory. if test ! -e $BASEDIR/$BACKUPKERNELDIR { return 0 } # If directory do exist, we only use if it has our # marker file. if test -d $BASEDIR/$BACKUPKERNELDIR -a \ -e $BASEDIR/$BACKUPKERNELDIR/.freebsd-update { return 0 } # We could not use current directory name, so add counter to # the end and try again. setglobal CNT = $shExpr('CNT + 1') if test $CNT -gt 9 { echo "Could not find valid backup dir ($BASEDIR/$BACKUPKERNELDIR)" exit 1 } setglobal BACKUPKERNELDIR = $[echo $BACKUPKERNELDIR | sed -Ee 's/[0-9]\$//] setglobal BACKUPKERNELDIR = ""$(BACKUPKERNELDIR)$(CNT)"" } } # Backup the current kernel using hardlinks, if not disabled by user. # Since we delete all files in the directory used for previous backups # we create a marker file called ".freebsd-update" in the directory so # we can determine on the next run that the directory was created by # freebsd-update and we then do not accidentally remove user files in # the unlikely case that the user has created a directory with a # conflicting name. proc backup_kernel { # Only make kernel backup is so configured. if test $BACKUPKERNEL != yes { return 0 } # Decide which directory name to use for kernel backups. backup_kernel_finddir # Remove old kernel backup files. If $BACKUPKERNELDIR was # "not ours", backup_kernel_finddir would have exited, so # deleting the directory content is as safe as we can make it. if test -d $BASEDIR/$BACKUPKERNELDIR { rm -fr $BASEDIR/$BACKUPKERNELDIR } # Create directories for backup. mkdir -p $BASEDIR/$BACKUPKERNELDIR mtree -cdn -p "$(BASEDIR)/$(KERNELDIR)" | \ mtree -Ue -p "$(BASEDIR)/$(BACKUPKERNELDIR)" > /dev/null # Mark the directory as having been created by freebsd-update. touch $BASEDIR/$BACKUPKERNELDIR/.freebsd-update if test $Status -ne 0 { echo "Could not create kernel backup directory" exit 1 } # Disable pathname expansion to be sure *.symbols is not # expanded. set -f # Use find to ignore symbol files, unless disabled by user. if test $BACKUPKERNELSYMBOLFILES = yes { setglobal FINDFILTER = ''"" } else { setglobal FINDFILTER = '"-a ! -name *.debug -a ! -name *.symbols'" } # Backup all the kernel files using hardlinks. shell {cd $(BASEDIR)/$(KERNELDIR) && find . -type f $FINDFILTER -exec \ cp -pl '{}' $(BASEDIR)/$(BACKUPKERNELDIR)/'{}' ';'} # Re-enable patchname expansion. set +f } # Install new files proc install_from_index { # First pass: Do everything apart from setting file flags. We # can't set flags yet, because schg inhibits hard linking. sort -k 1,1 -t '|' $1 | tr '|' ' ' | while read FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK { match $(TYPE) { with d # Create a directory install -d -o $(OWNER) -g $(GROUP) \ -m $(PERM) $(BASEDIR)/$(FPATH) with f if test -z $(LINK) { # Create a file, without setting flags. gunzip < files/$(HASH).gz > $(HASH) install -S -o $(OWNER) -g $(GROUP) \ -m $(PERM) $(HASH) $(BASEDIR)/$(FPATH) rm $(HASH) } else { # Create a hard link. ln -f $(BASEDIR)/$(LINK) $(BASEDIR)/$(FPATH) } with L # Create a symlink ln -sfh $(HASH) $(BASEDIR)/$(FPATH) } } # Perform a second pass, adding file flags. tr '|' ' ' < $1 | while read FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK { if test $(TYPE) = "f" && ! test $(FLAGS) = "0" { chflags $(FLAGS) $(BASEDIR)/$(FPATH) } } } # Remove files which we want to delete proc install_delete { # Generate list of new files cut -f 1 -d '|' < $2 | sort > newfiles # Generate subindex of old files we want to nuke sort -k 1,1 -t '|' $1 | join -t '|' -v 1 - newfiles | sort -r -k 1,1 -t '|' | cut -f 1,2 -d '|' | tr '|' ' ' > killfiles # Remove the offending bits while read FPATH TYPE { match $(TYPE) { with d rmdir $(BASEDIR)/$(FPATH) with f rm $(BASEDIR)/$(FPATH) with L rm $(BASEDIR)/$(FPATH) } } < killfiles # Clean up rm newfiles killfiles } # Install new files, delete old files, and update linker.hints proc install_files { # If we haven't already dealt with the kernel, deal with it. if ! test -f $1/kerneldone { grep -E '^/boot/' $1/INDEX-OLD > INDEX-OLD grep -E '^/boot/' $1/INDEX-NEW > INDEX-NEW # Backup current kernel before installing a new one backup_kernel || return 1 # Install new files install_from_index INDEX-NEW || return 1 # Remove files which need to be deleted install_delete INDEX-OLD INDEX-NEW || return 1 # Update linker.hints if necessary if test -s INDEX-OLD -o -s INDEX-NEW { kldxref -R $(BASEDIR)/boot/ !2 >/dev/null } # We've finished updating the kernel. touch $1/kerneldone # Do we need to ask for a reboot now? if test -f $1/kernelfirst && test -s INDEX-OLD -o -s INDEX-NEW { cat << """ Kernel updates have been installed. Please reboot and run "$0 install" again to finish installing updates. """ exit 0 } } # If we haven't already dealt with the world, deal with it. if ! test -f $1/worlddone { # Create any necessary directories first grep -vE '^/boot/' $1/INDEX-NEW | grep -E '^[^|]+\|d\|' > INDEX-NEW install_from_index INDEX-NEW || return 1 # Install new runtime linker grep -vE '^/boot/' $1/INDEX-NEW | grep -vE '^[^|]+\|d\|' | grep -E '^/libexec/ld-elf[^|]*\.so\.[0-9]+\|' > INDEX-NEW install_from_index INDEX-NEW || return 1 # Install new shared libraries next grep -vE '^/boot/' $1/INDEX-NEW | grep -vE '^[^|]+\|d\|' | grep -vE '^/libexec/ld-elf[^|]*\.so\.[0-9]+\|' | grep -E '^[^|]*/lib/[^|]*\.so\.[0-9]+\|' > INDEX-NEW install_from_index INDEX-NEW || return 1 # Deal with everything else grep -vE '^/boot/' $1/INDEX-OLD | grep -vE '^[^|]+\|d\|' | grep -vE '^/libexec/ld-elf[^|]*\.so\.[0-9]+\|' | grep -vE '^[^|]*/lib/[^|]*\.so\.[0-9]+\|' > INDEX-OLD grep -vE '^/boot/' $1/INDEX-NEW | grep -vE '^[^|]+\|d\|' | grep -vE '^/libexec/ld-elf[^|]*\.so\.[0-9]+\|' | grep -vE '^[^|]*/lib/[^|]*\.so\.[0-9]+\|' > INDEX-NEW install_from_index INDEX-NEW || return 1 install_delete INDEX-OLD INDEX-NEW || return 1 # Rebuild /etc/spwd.db and /etc/pwd.db if necessary. if test $(BASEDIR)/etc/master.passwd -nt $(BASEDIR)/etc/spwd.db || test $(BASEDIR)/etc/master.passwd -nt $(BASEDIR)/etc/pwd.db { pwd_mkdb -d $(BASEDIR)/etc $(BASEDIR)/etc/master.passwd } # Rebuild /etc/login.conf.db if necessary. if test $(BASEDIR)/etc/login.conf -nt $(BASEDIR)/etc/login.conf.db { cap_mkdb $(BASEDIR)/etc/login.conf } # We've finished installing the world and deleting old files # which are not shared libraries. touch $1/worlddone # Do we need to ask the user to portupgrade now? grep -vE '^/boot/' $1/INDEX-NEW | grep -E '^[^|]*/lib/[^|]*\.so\.[0-9]+\|' | cut -f 1 -d '|' | sort > newfiles if grep -vE '^/boot/' $1/INDEX-OLD | grep -E '^[^|]*/lib/[^|]*\.so\.[0-9]+\|' | cut -f 1 -d '|' | sort | join -v 1 - newfiles | grep -q . { cat << """ Completing this upgrade requires removing old shared object files. Please rebuild all installed 3rd party software (e.g., programs installed from the ports tree) and then run "$0 install" again to finish installing updates. """ rm newfiles exit 0 } rm newfiles } # Remove old shared libraries grep -vE '^/boot/' $1/INDEX-NEW | grep -vE '^[^|]+\|d\|' | grep -E '^[^|]*/lib/[^|]*\.so\.[0-9]+\|' > INDEX-NEW grep -vE '^/boot/' $1/INDEX-OLD | grep -vE '^[^|]+\|d\|' | grep -E '^[^|]*/lib/[^|]*\.so\.[0-9]+\|' > INDEX-OLD install_delete INDEX-OLD INDEX-NEW || return 1 # Remove old directories grep -vE '^/boot/' $1/INDEX-NEW | grep -E '^[^|]+\|d\|' > INDEX-NEW grep -vE '^/boot/' $1/INDEX-OLD | grep -E '^[^|]+\|d\|' > INDEX-OLD install_delete INDEX-OLD INDEX-NEW || return 1 # Remove temporary files rm INDEX-OLD INDEX-NEW } # Rearrange bits to allow the installed updates to be rolled back proc install_setup_rollback { # Remove the "reboot after installing kernel", "kernel updated", and # "finished installing the world" flags if present -- they are # irrelevant when rolling back updates. if test -f $(BDHASH)-install/kernelfirst { rm $(BDHASH)-install/kernelfirst rm $(BDHASH)-install/kerneldone } if test -f $(BDHASH)-install/worlddone { rm $(BDHASH)-install/worlddone } if test -L $(BDHASH)-rollback { mv $(BDHASH)-rollback $(BDHASH)-install/rollback } mv $(BDHASH)-install $(BDHASH)-rollback } # Actually install updates proc install_run { echo -n "Installing updates..." # Make sure we have all the files we should have install_verify $(BDHASH)-install/INDEX-OLD \ $(BDHASH)-install/INDEX-NEW || return 1 # Remove system immutable flag from files install_unschg $(BDHASH)-install/INDEX-OLD \ $(BDHASH)-install/INDEX-NEW || return 1 # Install new files, delete old files, and update linker.hints install_files $(BDHASH)-install || return 1 # Rearrange bits to allow the installed updates to be rolled back install_setup_rollback echo " done." } # Rearrange bits to allow the previous set of updates to be rolled back next. proc rollback_setup_rollback { if test -L $(BDHASH)-rollback/rollback { mv $(BDHASH)-rollback/rollback rollback-tmp rm -r $(BDHASH)-rollback/ rm $(BDHASH)-rollback mv rollback-tmp $(BDHASH)-rollback } else { rm -r $(BDHASH)-rollback/ rm $(BDHASH)-rollback } } # Install old files, delete new files, and update linker.hints proc rollback_files { # Install old shared library files which don't have the same path as # a new shared library file. grep -vE '^/boot/' $1/INDEX-NEW | grep -E '/lib/.*\.so\.[0-9]+\|' | cut -f 1 -d '|' | sort > INDEX-NEW.libs.flist grep -vE '^/boot/' $1/INDEX-OLD | grep -E '/lib/.*\.so\.[0-9]+\|' | sort -k 1,1 -t '|' - | join -t '|' -v 1 - INDEX-NEW.libs.flist > INDEX-OLD install_from_index INDEX-OLD || return 1 # Deal with files which are neither kernel nor shared library grep -vE '^/boot/' $1/INDEX-OLD | grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD grep -vE '^/boot/' $1/INDEX-NEW | grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW install_from_index INDEX-OLD || return 1 install_delete INDEX-NEW INDEX-OLD || return 1 # Install any old shared library files which we didn't install above. grep -vE '^/boot/' $1/INDEX-OLD | grep -E '/lib/.*\.so\.[0-9]+\|' | sort -k 1,1 -t '|' - | join -t '|' - INDEX-NEW.libs.flist > INDEX-OLD install_from_index INDEX-OLD || return 1 # Delete unneeded shared library files grep -vE '^/boot/' $1/INDEX-OLD | grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD grep -vE '^/boot/' $1/INDEX-NEW | grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW install_delete INDEX-NEW INDEX-OLD || return 1 # Deal with kernel files grep -E '^/boot/' $1/INDEX-OLD > INDEX-OLD grep -E '^/boot/' $1/INDEX-NEW > INDEX-NEW install_from_index INDEX-OLD || return 1 install_delete INDEX-NEW INDEX-OLD || return 1 if test -s INDEX-OLD -o -s INDEX-NEW { kldxref -R /boot/ !2 >/dev/null } # Remove temporary files rm INDEX-OLD INDEX-NEW INDEX-NEW.libs.flist } # Actually rollback updates proc rollback_run { echo -n "Uninstalling updates..." # If there are updates waiting to be installed, remove them; we # want the user to re-run 'fetch' after rolling back updates. if test -L $(BDHASH)-install { rm -r $(BDHASH)-install/ rm $(BDHASH)-install } # Make sure we have all the files we should have install_verify $(BDHASH)-rollback/INDEX-NEW \ $(BDHASH)-rollback/INDEX-OLD || return 1 # Remove system immutable flag from files install_unschg $(BDHASH)-rollback/INDEX-NEW \ $(BDHASH)-rollback/INDEX-OLD || return 1 # Install old files, delete new files, and update linker.hints rollback_files $(BDHASH)-rollback || return 1 # Remove the rollback directory and the symlink pointing to it; and # rearrange bits to allow the previous set of updates to be rolled # back next. rollback_setup_rollback echo " done." } # Compare INDEX-ALL and INDEX-PRESENT and print warnings about differences. proc IDS_compare { # Get all the lines which mismatch in something other than file # flags. We ignore file flags because sysinstall doesn't seem to # set them when it installs FreeBSD; warning about these adds a # very large amount of noise. cut -f 1-5,7-8 -d '|' $1 > $1.noflags sort -k 1,1 -t '|' $1.noflags > $1.sorted cut -f 1-5,7-8 -d '|' $2 | comm -13 $1.noflags - | fgrep -v '|-|||||' | sort -k 1,1 -t '|' | join -t '|' $1.sorted - > INDEX-NOTMATCHING # Ignore files which match IDSIGNOREPATHS. for X in [$(IDSIGNOREPATHS)] { grep -E "^$(X)" INDEX-NOTMATCHING } | sort -u | comm -13 - INDEX-NOTMATCHING > INDEX-NOTMATCHING.tmp mv INDEX-NOTMATCHING.tmp INDEX-NOTMATCHING # Go through the lines and print warnings. local IFS='|' while read FPATH TYPE OWNER GROUP PERM HASH LINK P_TYPE P_OWNER P_GROUP P_PERM P_HASH P_LINK { # Warn about different object types. if ! test $(TYPE) = $(P_TYPE) { echo -n "$(FPATH) is a " match $(P_TYPE) { with f echo -n "regular file, " with d echo -n "directory, " with L echo -n "symlink, " } echo -n "but should be a " match $(TYPE) { with f echo -n "regular file." with d echo -n "directory." with L echo -n "symlink." } echo # Skip other tests, since they don't make sense if # we're comparing different object types. continue } # Warn about different owners. if ! test $(OWNER) = $(P_OWNER) { echo -n "$(FPATH) is owned by user id $(P_OWNER), " echo "but should be owned by user id $(OWNER)." } # Warn about different groups. if ! test $(GROUP) = $(P_GROUP) { echo -n "$(FPATH) is owned by group id $(P_GROUP), " echo "but should be owned by group id $(GROUP)." } # Warn about different permissions. We do not warn about # different permissions on symlinks, since some archivers # don't extract symlink permissions correctly and they are # ignored anyway. if ! test $(PERM) = $(P_PERM) && ! test $(TYPE) = "L" { echo -n "$(FPATH) has $(P_PERM) permissions, " echo "but should have $(PERM) permissions." } # Warn about different file hashes / symlink destinations. if ! test $(HASH) = $(P_HASH) { if test $(TYPE) = "L" { echo -n "$(FPATH) is a symlink to $(P_HASH), " echo "but should be a symlink to $(HASH)." } if test $(TYPE) = "f" { echo -n "$(FPATH) has SHA256 hash $(P_HASH), " echo "but should have SHA256 hash $(HASH)." } } # We don't warn about different hard links, since some # some archivers break hard links, and as long as the # underlying data is correct they really don't matter. } < INDEX-NOTMATCHING # Clean up rm $1 $1.noflags $1.sorted $2 INDEX-NOTMATCHING } # Do the work involved in comparing the system to a "known good" index proc IDS_run { workdir_init || return 1 # Prepare the mirror list. fetch_pick_server_init && fetch_pick_server # Try to fetch the public key until we run out of servers. while ! fetch_key { fetch_pick_server || return 1 } # Try to fetch the metadata index signature ("tag") until we run # out of available servers; and sanity check the downloaded tag. while ! fetch_tag { fetch_pick_server || return 1 } fetch_tagsanity || return 1 # Fetch INDEX-OLD and INDEX-ALL. fetch_metadata INDEX-OLD INDEX-ALL || return 1 # Generate filtered INDEX-OLD and INDEX-ALL files containing only # the components we want and without anything marked as "Ignore". fetch_filter_metadata INDEX-OLD || return 1 fetch_filter_metadata INDEX-ALL || return 1 # Merge the INDEX-OLD and INDEX-ALL files into INDEX-ALL. sort INDEX-OLD INDEX-ALL > INDEX-ALL.tmp mv INDEX-ALL.tmp INDEX-ALL rm INDEX-OLD # Translate /boot/${KERNCONF} to ${KERNELDIR} fetch_filter_kernel_names INDEX-ALL $(KERNCONF) # Inspect the system and generate an INDEX-PRESENT file. fetch_inspect_system INDEX-ALL INDEX-PRESENT /dev/null || return 1 # Compare INDEX-ALL and INDEX-PRESENT and print warnings about any # differences. IDS_compare INDEX-ALL INDEX-PRESENT } #### Main functions -- call parameter-handling and core functions # Using the command line, configuration file, and defaults, # set all the parameters which are needed later. proc get_params { init_params parse_cmdline $ifsjoin(Argv) parse_conffile default_params } # Fetch command. Make sure that we're being called # interactively, then run fetch_check_params and fetch_run proc cmd_fetch { if test ! -t 0 -a $NOTTYOK -eq 0 { echo -n "$[basename $0] fetch should not " echo "be run non-interactively." echo "Run $[basename $0] cron instead." exit 1 } fetch_check_params fetch_run || exit 1 } # Cron command. Make sure the parameters are sensible; wait # rand(3600) seconds; then fetch updates. While fetching updates, # send output to a temporary file; only print that file if the # fetching failed. proc cmd_cron { fetch_check_params sleep $[jot -r 1 0 3600] setglobal TMPFILE = $[mktemp /tmp/freebsd-update.XXXXXX] || exit 1 if ! fetch_run >> $(TMPFILE) || ! grep -q "No updates needed" $(TMPFILE) || test $(VERBOSELEVEL) = "debug" { mail -s "$[hostname] security updates" $(MAILTO) < $(TMPFILE) } rm $(TMPFILE) } # Fetch files for upgrading to a new release. proc cmd_upgrade { upgrade_check_params upgrade_run || exit 1 } # Install downloaded updates. proc cmd_install { install_check_params install_run || exit 1 } # Rollback most recently installed updates. proc cmd_rollback { rollback_check_params rollback_run || exit 1 } # Compare system against a "known good" index. proc cmd_IDS { IDS_check_params IDS_run || exit 1 } #### Entry point # Make sure we find utilities from the base system export PATH=/sbin:/bin:/usr/sbin:/usr/bin:$(PATH) # Set a pager if the user doesn't if test -z $PAGER { setglobal PAGER = '/usr/bin/more' } # Set LC_ALL in order to avoid problems with character ranges like [A-Z]. export LC_ALL=C get_params $ifsjoin(Argv) for COMMAND in [$(COMMANDS)] { cmd_$(COMMAND) }