]> CyberLeo.Net >> Repos - FreeBSD/releng/7.2.git/blob - usr.sbin/freebsd-update/freebsd-update.sh
Disable SSL renegotiation in order to protect against a serious
[FreeBSD/releng/7.2.git] / usr.sbin / freebsd-update / freebsd-update.sh
1 #!/bin/sh
2
3 #-
4 # Copyright 2004-2007 Colin Percival
5 # All rights reserved
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted providing that the following conditions 
9 # are met:
10 # 1. Redistributions of source code must retain the above copyright
11 #    notice, this list of conditions and the following disclaimer.
12 # 2. Redistributions in binary form must reproduce the above copyright
13 #    notice, this list of conditions and the following disclaimer in the
14 #    documentation and/or other materials provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
20 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
24 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
25 # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 # POSSIBILITY OF SUCH DAMAGE.
27
28 # $FreeBSD$
29
30 #### Usage function -- called from command-line handling code.
31
32 # Usage instructions.  Options not listed:
33 # --debug       -- don't filter output from utilities
34 # --no-stats    -- don't show progress statistics while fetching files
35 usage () {
36         cat <<EOF
37 usage: `basename $0` [options] command ... [path]
38
39 Options:
40   -b basedir   -- Operate on a system mounted at basedir
41                   (default: /)
42   -d workdir   -- Store working files in workdir
43                   (default: /var/db/freebsd-update/)
44   -f conffile  -- Read configuration options from conffile
45                   (default: /etc/freebsd-update.conf)
46   -k KEY       -- Trust an RSA key with SHA256 hash of KEY
47   -r release   -- Target for upgrade (e.g., 6.2-RELEASE)
48   -s server    -- Server from which to fetch updates
49                   (default: update.FreeBSD.org)
50   -t address   -- Mail output of cron command, if any, to address
51                   (default: root)
52 Commands:
53   fetch        -- Fetch updates from server
54   cron         -- Sleep rand(3600) seconds, fetch updates, and send an
55                   email if updates were found
56   upgrade      -- Fetch upgrades to FreeBSD version specified via -r option
57   install      -- Install downloaded updates or upgrades
58   rollback     -- Uninstall most recently installed updates
59   IDS          -- Compare the system against an index of "known good" files.
60 EOF
61         exit 0
62 }
63
64 #### Configuration processing functions
65
66 #-
67 # Configuration options are set in the following order of priority:
68 # 1. Command line options
69 # 2. Configuration file options
70 # 3. Default options
71 # In addition, certain options (e.g., IgnorePaths) can be specified multiple
72 # times and (as long as these are all in the same place, e.g., inside the
73 # configuration file) they will accumulate.  Finally, because the path to the
74 # configuration file can be specified at the command line, the entire command
75 # line must be processed before we start reading the configuration file.
76 #
77 # Sound like a mess?  It is.  Here's how we handle this:
78 # 1. Initialize CONFFILE and all the options to "".
79 # 2. Process the command line.  Throw an error if a non-accumulating option
80 #    is specified twice.
81 # 3. If CONFFILE is "", set CONFFILE to /etc/freebsd-update.conf .
82 # 4. For all the configuration options X, set X_saved to X.
83 # 5. Initialize all the options to "".
84 # 6. Read CONFFILE line by line, parsing options.
85 # 7. For each configuration option X, set X to X_saved iff X_saved is not "".
86 # 8. Repeat steps 4-7, except setting options to their default values at (6).
87
88 CONFIGOPTIONS="KEYPRINT WORKDIR SERVERNAME MAILTO ALLOWADD ALLOWDELETE
89     KEEPMODIFIEDMETADATA COMPONENTS IGNOREPATHS UPDATEIFUNMODIFIED
90     BASEDIR VERBOSELEVEL TARGETRELEASE STRICTCOMPONENTS MERGECHANGES
91     IDSIGNOREPATHS"
92
93 # Set all the configuration options to "".
94 nullconfig () {
95         for X in ${CONFIGOPTIONS}; do
96                 eval ${X}=""
97         done
98 }
99
100 # For each configuration option X, set X_saved to X.
101 saveconfig () {
102         for X in ${CONFIGOPTIONS}; do
103                 eval ${X}_saved=\$${X}
104         done
105 }
106
107 # For each configuration option X, set X to X_saved if X_saved is not "".
108 mergeconfig () {
109         for X in ${CONFIGOPTIONS}; do
110                 eval _=\$${X}_saved
111                 if ! [ -z "${_}" ]; then
112                         eval ${X}=\$${X}_saved
113                 fi
114         done
115 }
116
117 # Set the trusted keyprint.
118 config_KeyPrint () {
119         if [ -z ${KEYPRINT} ]; then
120                 KEYPRINT=$1
121         else
122                 return 1
123         fi
124 }
125
126 # Set the working directory.
127 config_WorkDir () {
128         if [ -z ${WORKDIR} ]; then
129                 WORKDIR=$1
130         else
131                 return 1
132         fi
133 }
134
135 # Set the name of the server (pool) from which to fetch updates
136 config_ServerName () {
137         if [ -z ${SERVERNAME} ]; then
138                 SERVERNAME=$1
139         else
140                 return 1
141         fi
142 }
143
144 # Set the address to which 'cron' output will be mailed.
145 config_MailTo () {
146         if [ -z ${MAILTO} ]; then
147                 MAILTO=$1
148         else
149                 return 1
150         fi
151 }
152
153 # Set whether FreeBSD Update is allowed to add files (or directories, or
154 # symlinks) which did not previously exist.
155 config_AllowAdd () {
156         if [ -z ${ALLOWADD} ]; then
157                 case $1 in
158                 [Yy][Ee][Ss])
159                         ALLOWADD=yes
160                         ;;
161                 [Nn][Oo])
162                         ALLOWADD=no
163                         ;;
164                 *)
165                         return 1
166                         ;;
167                 esac
168         else
169                 return 1
170         fi
171 }
172
173 # Set whether FreeBSD Update is allowed to remove files/directories/symlinks.
174 config_AllowDelete () {
175         if [ -z ${ALLOWDELETE} ]; then
176                 case $1 in
177                 [Yy][Ee][Ss])
178                         ALLOWDELETE=yes
179                         ;;
180                 [Nn][Oo])
181                         ALLOWDELETE=no
182                         ;;
183                 *)
184                         return 1
185                         ;;
186                 esac
187         else
188                 return 1
189         fi
190 }
191
192 # Set whether FreeBSD Update should keep existing inode ownership,
193 # permissions, and flags, in the event that they have been modified locally
194 # after the release.
195 config_KeepModifiedMetadata () {
196         if [ -z ${KEEPMODIFIEDMETADATA} ]; then
197                 case $1 in
198                 [Yy][Ee][Ss])
199                         KEEPMODIFIEDMETADATA=yes
200                         ;;
201                 [Nn][Oo])
202                         KEEPMODIFIEDMETADATA=no
203                         ;;
204                 *)
205                         return 1
206                         ;;
207                 esac
208         else
209                 return 1
210         fi
211 }
212
213 # Add to the list of components which should be kept updated.
214 config_Components () {
215         for C in $@; do
216                 COMPONENTS="${COMPONENTS} ${C}"
217         done
218 }
219
220 # Add to the list of paths under which updates will be ignored.
221 config_IgnorePaths () {
222         for C in $@; do
223                 IGNOREPATHS="${IGNOREPATHS} ${C}"
224         done
225 }
226
227 # Add to the list of paths which IDS should ignore.
228 config_IDSIgnorePaths () {
229         for C in $@; do
230                 IDSIGNOREPATHS="${IDSIGNOREPATHS} ${C}"
231         done
232 }
233
234 # Add to the list of paths within which updates will be performed only if the
235 # file on disk has not been modified locally.
236 config_UpdateIfUnmodified () {
237         for C in $@; do
238                 UPDATEIFUNMODIFIED="${UPDATEIFUNMODIFIED} ${C}"
239         done
240 }
241
242 # Add to the list of paths within which updates to text files will be merged
243 # instead of overwritten.
244 config_MergeChanges () {
245         for C in $@; do
246                 MERGECHANGES="${MERGECHANGES} ${C}"
247         done
248 }
249
250 # Work on a FreeBSD installation mounted under $1
251 config_BaseDir () {
252         if [ -z ${BASEDIR} ]; then
253                 BASEDIR=$1
254         else
255                 return 1
256         fi
257 }
258
259 # When fetching upgrades, should we assume the user wants exactly the
260 # components listed in COMPONENTS, rather than trying to guess based on
261 # what's currently installed?
262 config_StrictComponents () {
263         if [ -z ${STRICTCOMPONENTS} ]; then
264                 case $1 in
265                 [Yy][Ee][Ss])
266                         STRICTCOMPONENTS=yes
267                         ;;
268                 [Nn][Oo])
269                         STRICTCOMPONENTS=no
270                         ;;
271                 *)
272                         return 1
273                         ;;
274                 esac
275         else
276                 return 1
277         fi
278 }
279
280 # Upgrade to FreeBSD $1
281 config_TargetRelease () {
282         if [ -z ${TARGETRELEASE} ]; then
283                 TARGETRELEASE=$1
284         else
285                 return 1
286         fi
287 }
288
289 # Define what happens to output of utilities
290 config_VerboseLevel () {
291         if [ -z ${VERBOSELEVEL} ]; then
292                 case $1 in
293                 [Dd][Ee][Bb][Uu][Gg])
294                         VERBOSELEVEL=debug
295                         ;;
296                 [Nn][Oo][Ss][Tt][Aa][Tt][Ss])
297                         VERBOSELEVEL=nostats
298                         ;;
299                 [Ss][Tt][Aa][Tt][Ss])
300                         VERBOSELEVEL=stats
301                         ;;
302                 *)
303                         return 1
304                         ;;
305                 esac
306         else
307                 return 1
308         fi
309 }
310
311 # Handle one line of configuration
312 configline () {
313         if [ $# -eq 0 ]; then
314                 return
315         fi
316
317         OPT=$1
318         shift
319         config_${OPT} $@
320 }
321
322 #### Parameter handling functions.
323
324 # Initialize parameters to null, just in case they're
325 # set in the environment.
326 init_params () {
327         # Configration settings
328         nullconfig
329
330         # No configuration file set yet
331         CONFFILE=""
332
333         # No commands specified yet
334         COMMANDS=""
335 }
336
337 # Parse the command line
338 parse_cmdline () {
339         while [ $# -gt 0 ]; do
340                 case "$1" in
341                 # Location of configuration file
342                 -f)
343                         if [ $# -eq 1 ]; then usage; fi
344                         if [ ! -z "${CONFFILE}" ]; then usage; fi
345                         shift; CONFFILE="$1"
346                         ;;
347
348                 # Configuration file equivalents
349                 -b)
350                         if [ $# -eq 1 ]; then usage; fi; shift
351                         config_BaseDir $1 || usage
352                         ;;
353                 -d)
354                         if [ $# -eq 1 ]; then usage; fi; shift
355                         config_WorkDir $1 || usage
356                         ;;
357                 -k)
358                         if [ $# -eq 1 ]; then usage; fi; shift
359                         config_KeyPrint $1 || usage
360                         ;;
361                 -s)
362                         if [ $# -eq 1 ]; then usage; fi; shift
363                         config_ServerName $1 || usage
364                         ;;
365                 -r)
366                         if [ $# -eq 1 ]; then usage; fi; shift
367                         config_TargetRelease $1 || usage
368                         ;;
369                 -t)
370                         if [ $# -eq 1 ]; then usage; fi; shift
371                         config_MailTo $1 || usage
372                         ;;
373                 -v)
374                         if [ $# -eq 1 ]; then usage; fi; shift
375                         config_VerboseLevel $1 || usage
376                         ;;
377
378                 # Aliases for "-v debug" and "-v nostats"
379                 --debug)
380                         config_VerboseLevel debug || usage
381                         ;;
382                 --no-stats)
383                         config_VerboseLevel nostats || usage
384                         ;;
385
386                 # Commands
387                 cron | fetch | upgrade | install | rollback | IDS)
388                         COMMANDS="${COMMANDS} $1"
389                         ;;
390
391                 # Anything else is an error
392                 *)
393                         usage
394                         ;;
395                 esac
396                 shift
397         done
398
399         # Make sure we have at least one command
400         if [ -z "${COMMANDS}" ]; then
401                 usage
402         fi
403 }
404
405 # Parse the configuration file
406 parse_conffile () {
407         # If a configuration file was specified on the command line, check
408         # that it exists and is readable.
409         if [ ! -z "${CONFFILE}" ] && [ ! -r "${CONFFILE}" ]; then
410                 echo -n "File does not exist "
411                 echo -n "or is not readable: "
412                 echo ${CONFFILE}
413                 exit 1
414         fi
415
416         # If a configuration file was not specified on the command line,
417         # use the default configuration file path.  If that default does
418         # not exist, give up looking for any configuration.
419         if [ -z "${CONFFILE}" ]; then
420                 CONFFILE="/etc/freebsd-update.conf"
421                 if [ ! -r "${CONFFILE}" ]; then
422                         return
423                 fi
424         fi
425
426         # Save the configuration options specified on the command line, and
427         # clear all the options in preparation for reading the config file.
428         saveconfig
429         nullconfig
430
431         # Read the configuration file.  Anything after the first '#' is
432         # ignored, and any blank lines are ignored.
433         L=0
434         while read LINE; do
435                 L=$(($L + 1))
436                 LINEX=`echo "${LINE}" | cut -f 1 -d '#'`
437                 if ! configline ${LINEX}; then
438                         echo "Error processing configuration file, line $L:"
439                         echo "==> ${LINE}"
440                         exit 1
441                 fi
442         done < ${CONFFILE}
443
444         # Merge the settings read from the configuration file with those
445         # provided at the command line.
446         mergeconfig
447 }
448
449 # Provide some default parameters
450 default_params () {
451         # Save any parameters already configured, and clear the slate
452         saveconfig
453         nullconfig
454
455         # Default configurations
456         config_WorkDir /var/db/freebsd-update
457         config_MailTo root
458         config_AllowAdd yes
459         config_AllowDelete yes
460         config_KeepModifiedMetadata yes
461         config_BaseDir /
462         config_VerboseLevel stats
463         config_StrictComponents no
464
465         # Merge these defaults into the earlier-configured settings
466         mergeconfig
467 }
468
469 # Set utility output filtering options, based on ${VERBOSELEVEL}
470 fetch_setup_verboselevel () {
471         case ${VERBOSELEVEL} in
472         debug)
473                 QUIETREDIR="/dev/stderr"
474                 QUIETFLAG=" "
475                 STATSREDIR="/dev/stderr"
476                 DDSTATS=".."
477                 XARGST="-t"
478                 NDEBUG=" "
479                 ;;
480         nostats)
481                 QUIETREDIR=""
482                 QUIETFLAG=""
483                 STATSREDIR="/dev/null"
484                 DDSTATS=".."
485                 XARGST=""
486                 NDEBUG=""
487                 ;;
488         stats)
489                 QUIETREDIR="/dev/null"
490                 QUIETFLAG="-q"
491                 STATSREDIR="/dev/stdout"
492                 DDSTATS=""
493                 XARGST=""
494                 NDEBUG="-n"
495                 ;;
496         esac
497 }
498
499 # Perform sanity checks and set some final parameters
500 # in preparation for fetching files.  Figure out which
501 # set of updates should be downloaded: If the user is
502 # running *-p[0-9]+, strip off the last part; if the
503 # user is running -SECURITY, call it -RELEASE.  Chdir
504 # into the working directory.
505 fetch_check_params () {
506         export HTTP_USER_AGENT="freebsd-update (${COMMAND}, `uname -r`)"
507
508         _SERVERNAME_z=\
509 "SERVERNAME must be given via command line or configuration file."
510         _KEYPRINT_z="Key must be given via -k option or configuration file."
511         _KEYPRINT_bad="Invalid key fingerprint: "
512         _WORKDIR_bad="Directory does not exist or is not writable: "
513
514         if [ -z "${SERVERNAME}" ]; then
515                 echo -n "`basename $0`: "
516                 echo "${_SERVERNAME_z}"
517                 exit 1
518         fi
519         if [ -z "${KEYPRINT}" ]; then
520                 echo -n "`basename $0`: "
521                 echo "${_KEYPRINT_z}"
522                 exit 1
523         fi
524         if ! echo "${KEYPRINT}" | grep -qE "^[0-9a-f]{64}$"; then
525                 echo -n "`basename $0`: "
526                 echo -n "${_KEYPRINT_bad}"
527                 echo ${KEYPRINT}
528                 exit 1
529         fi
530         if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
531                 echo -n "`basename $0`: "
532                 echo -n "${_WORKDIR_bad}"
533                 echo ${WORKDIR}
534                 exit 1
535         fi
536         chmod 700 ${WORKDIR}
537         cd ${WORKDIR} || exit 1
538
539         # Generate release number.  The s/SECURITY/RELEASE/ bit exists
540         # to provide an upgrade path for FreeBSD Update 1.x users, since
541         # the kernels provided by FreeBSD Update 1.x are always labelled
542         # as X.Y-SECURITY.
543         RELNUM=`uname -r |
544             sed -E 's,-p[0-9]+,,' |
545             sed -E 's,-SECURITY,-RELEASE,'`
546         ARCH=`uname -m`
547         FETCHDIR=${RELNUM}/${ARCH}
548         PATCHDIR=${RELNUM}/${ARCH}/bp
549
550         # Figure out what directory contains the running kernel
551         BOOTFILE=`sysctl -n kern.bootfile`
552         KERNELDIR=${BOOTFILE%/kernel}
553         if ! [ -d ${KERNELDIR} ]; then
554                 echo "Cannot identify running kernel"
555                 exit 1
556         fi
557
558         # Figure out what kernel configuration is running.  We start with
559         # the output of `uname -i`, and then make the following adjustments:
560         # 1. Replace "SMP-GENERIC" with "SMP".  Why the SMP kernel config
561         # file says "ident SMP-GENERIC", I don't know...
562         # 2. If the kernel claims to be GENERIC _and_ ${ARCH} is "amd64"
563         # _and_ `sysctl kern.version` contains a line which ends "/SMP", then
564         # we're running an SMP kernel.  This mis-identification is a bug
565         # which was fixed in 6.2-STABLE.
566         KERNCONF=`uname -i`
567         if [ ${KERNCONF} = "SMP-GENERIC" ]; then
568                 KERNCONF=SMP
569         fi
570         if [ ${KERNCONF} = "GENERIC" ] && [ ${ARCH} = "amd64" ]; then
571                 if sysctl kern.version | grep -qE '/SMP$'; then
572                         KERNCONF=SMP
573                 fi
574         fi
575
576         # Define some paths
577         BSPATCH=/usr/bin/bspatch
578         SHA256=/sbin/sha256
579         PHTTPGET=/usr/libexec/phttpget
580
581         # Set up variables relating to VERBOSELEVEL
582         fetch_setup_verboselevel
583
584         # Construct a unique name from ${BASEDIR}
585         BDHASH=`echo ${BASEDIR} | sha256 -q`
586 }
587
588 # Perform sanity checks etc. before fetching upgrades.
589 upgrade_check_params () {
590         fetch_check_params
591
592         # Unless set otherwise, we're upgrading to the same kernel config.
593         NKERNCONF=${KERNCONF}
594
595         # We need TARGETRELEASE set
596         _TARGETRELEASE_z="Release target must be specified via -r option."
597         if [ -z "${TARGETRELEASE}" ]; then
598                 echo -n "`basename $0`: "
599                 echo "${_TARGETRELEASE_z}"
600                 exit 1
601         fi
602
603         # The target release should be != the current release.
604         if [ "${TARGETRELEASE}" = "${RELNUM}" ]; then
605                 echo -n "`basename $0`: "
606                 echo "Cannot upgrade from ${RELNUM} to itself"
607                 exit 1
608         fi
609
610         # Turning off AllowAdd or AllowDelete is a bad idea for upgrades.
611         if [ "${ALLOWADD}" = "no" ]; then
612                 echo -n "`basename $0`: "
613                 echo -n "WARNING: \"AllowAdd no\" is a bad idea "
614                 echo "when upgrading between releases."
615                 echo
616         fi
617         if [ "${ALLOWDELETE}" = "no" ]; then
618                 echo -n "`basename $0`: "
619                 echo -n "WARNING: \"AllowDelete no\" is a bad idea "
620                 echo "when upgrading between releases."
621                 echo
622         fi
623
624         # Set EDITOR to /usr/bin/vi if it isn't already set
625         : ${EDITOR:='/usr/bin/vi'}
626 }
627
628 # Perform sanity checks and set some final parameters in
629 # preparation for installing updates.
630 install_check_params () {
631         # Check that we are root.  All sorts of things won't work otherwise.
632         if [ `id -u` != 0 ]; then
633                 echo "You must be root to run this."
634                 exit 1
635         fi
636
637         # Check that securelevel <= 0.  Otherwise we can't update schg files.
638         if [ `sysctl -n kern.securelevel` -gt 0 ]; then
639                 echo "Updates cannot be installed when the system securelevel"
640                 echo "is greater than zero."
641                 exit 1
642         fi
643
644         # Check that we have a working directory
645         _WORKDIR_bad="Directory does not exist or is not writable: "
646         if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
647                 echo -n "`basename $0`: "
648                 echo -n "${_WORKDIR_bad}"
649                 echo ${WORKDIR}
650                 exit 1
651         fi
652         cd ${WORKDIR} || exit 1
653
654         # Construct a unique name from ${BASEDIR}
655         BDHASH=`echo ${BASEDIR} | sha256 -q`
656
657         # Check that we have updates ready to install
658         if ! [ -L ${BDHASH}-install ]; then
659                 echo "No updates are available to install."
660                 echo "Run '$0 fetch' first."
661                 exit 1
662         fi
663         if ! [ -f ${BDHASH}-install/INDEX-OLD ] ||
664             ! [ -f ${BDHASH}-install/INDEX-NEW ]; then
665                 echo "Update manifest is corrupt -- this should never happen."
666                 echo "Re-run '$0 fetch'."
667                 exit 1
668         fi
669 }
670
671 # Perform sanity checks and set some final parameters in
672 # preparation for UNinstalling updates.
673 rollback_check_params () {
674         # Check that we are root.  All sorts of things won't work otherwise.
675         if [ `id -u` != 0 ]; then
676                 echo "You must be root to run this."
677                 exit 1
678         fi
679
680         # Check that we have a working directory
681         _WORKDIR_bad="Directory does not exist or is not writable: "
682         if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
683                 echo -n "`basename $0`: "
684                 echo -n "${_WORKDIR_bad}"
685                 echo ${WORKDIR}
686                 exit 1
687         fi
688         cd ${WORKDIR} || exit 1
689
690         # Construct a unique name from ${BASEDIR}
691         BDHASH=`echo ${BASEDIR} | sha256 -q`
692
693         # Check that we have updates ready to rollback
694         if ! [ -L ${BDHASH}-rollback ]; then
695                 echo "No rollback directory found."
696                 exit 1
697         fi
698         if ! [ -f ${BDHASH}-rollback/INDEX-OLD ] ||
699             ! [ -f ${BDHASH}-rollback/INDEX-NEW ]; then
700                 echo "Update manifest is corrupt -- this should never happen."
701                 exit 1
702         fi
703 }
704
705 # Perform sanity checks and set some final parameters
706 # in preparation for comparing the system against the
707 # published index.  Figure out which index we should
708 # compare against: If the user is running *-p[0-9]+,
709 # strip off the last part; if the user is running
710 # -SECURITY, call it -RELEASE.  Chdir into the working
711 # directory.
712 IDS_check_params () {
713         export HTTP_USER_AGENT="freebsd-update (${COMMAND}, `uname -r`)"
714
715         _SERVERNAME_z=\
716 "SERVERNAME must be given via command line or configuration file."
717         _KEYPRINT_z="Key must be given via -k option or configuration file."
718         _KEYPRINT_bad="Invalid key fingerprint: "
719         _WORKDIR_bad="Directory does not exist or is not writable: "
720
721         if [ -z "${SERVERNAME}" ]; then
722                 echo -n "`basename $0`: "
723                 echo "${_SERVERNAME_z}"
724                 exit 1
725         fi
726         if [ -z "${KEYPRINT}" ]; then
727                 echo -n "`basename $0`: "
728                 echo "${_KEYPRINT_z}"
729                 exit 1
730         fi
731         if ! echo "${KEYPRINT}" | grep -qE "^[0-9a-f]{64}$"; then
732                 echo -n "`basename $0`: "
733                 echo -n "${_KEYPRINT_bad}"
734                 echo ${KEYPRINT}
735                 exit 1
736         fi
737         if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
738                 echo -n "`basename $0`: "
739                 echo -n "${_WORKDIR_bad}"
740                 echo ${WORKDIR}
741                 exit 1
742         fi
743         cd ${WORKDIR} || exit 1
744
745         # Generate release number.  The s/SECURITY/RELEASE/ bit exists
746         # to provide an upgrade path for FreeBSD Update 1.x users, since
747         # the kernels provided by FreeBSD Update 1.x are always labelled
748         # as X.Y-SECURITY.
749         RELNUM=`uname -r |
750             sed -E 's,-p[0-9]+,,' |
751             sed -E 's,-SECURITY,-RELEASE,'`
752         ARCH=`uname -m`
753         FETCHDIR=${RELNUM}/${ARCH}
754         PATCHDIR=${RELNUM}/${ARCH}/bp
755
756         # Figure out what directory contains the running kernel
757         BOOTFILE=`sysctl -n kern.bootfile`
758         KERNELDIR=${BOOTFILE%/kernel}
759         if ! [ -d ${KERNELDIR} ]; then
760                 echo "Cannot identify running kernel"
761                 exit 1
762         fi
763
764         # Figure out what kernel configuration is running.  We start with
765         # the output of `uname -i`, and then make the following adjustments:
766         # 1. Replace "SMP-GENERIC" with "SMP".  Why the SMP kernel config
767         # file says "ident SMP-GENERIC", I don't know...
768         # 2. If the kernel claims to be GENERIC _and_ ${ARCH} is "amd64"
769         # _and_ `sysctl kern.version` contains a line which ends "/SMP", then
770         # we're running an SMP kernel.  This mis-identification is a bug
771         # which was fixed in 6.2-STABLE.
772         KERNCONF=`uname -i`
773         if [ ${KERNCONF} = "SMP-GENERIC" ]; then
774                 KERNCONF=SMP
775         fi
776         if [ ${KERNCONF} = "GENERIC" ] && [ ${ARCH} = "amd64" ]; then
777                 if sysctl kern.version | grep -qE '/SMP$'; then
778                         KERNCONF=SMP
779                 fi
780         fi
781
782         # Define some paths
783         SHA256=/sbin/sha256
784         PHTTPGET=/usr/libexec/phttpget
785
786         # Set up variables relating to VERBOSELEVEL
787         fetch_setup_verboselevel
788 }
789
790 #### Core functionality -- the actual work gets done here
791
792 # Use an SRV query to pick a server.  If the SRV query doesn't provide
793 # a useful answer, use the server name specified by the user.
794 # Put another way... look up _http._tcp.${SERVERNAME} and pick a server
795 # from that; or if no servers are returned, use ${SERVERNAME}.
796 # This allows a user to specify "portsnap.freebsd.org" (in which case
797 # portsnap will select one of the mirrors) or "portsnap5.tld.freebsd.org"
798 # (in which case portsnap will use that particular server, since there
799 # won't be an SRV entry for that name).
800 #
801 # We ignore the Port field, since we are always going to use port 80.
802
803 # Fetch the mirror list, but do not pick a mirror yet.  Returns 1 if
804 # no mirrors are available for any reason.
805 fetch_pick_server_init () {
806         : > serverlist_tried
807
808 # Check that host(1) exists (i.e., that the system wasn't built with the
809 # WITHOUT_BIND set) and don't try to find a mirror if it doesn't exist.
810         if ! which -s host; then
811                 : > serverlist_full
812                 return 1
813         fi
814
815         echo -n "Looking up ${SERVERNAME} mirrors... "
816
817 # Issue the SRV query and pull out the Priority, Weight, and Target fields.
818 # BIND 9 prints "$name has SRV record ..." while BIND 8 prints
819 # "$name server selection ..."; we allow either format.
820         MLIST="_http._tcp.${SERVERNAME}"
821         host -t srv "${MLIST}" |
822             sed -nE "s/${MLIST} (has SRV record|server selection) //p" |
823             cut -f 1,2,4 -d ' ' |
824             sed -e 's/\.$//' |
825             sort > serverlist_full
826
827 # If no records, give up -- we'll just use the server name we were given.
828         if [ `wc -l < serverlist_full` -eq 0 ]; then
829                 echo "none found."
830                 return 1
831         fi
832
833 # Report how many mirrors we found.
834         echo `wc -l < serverlist_full` "mirrors found."
835
836 # Generate a random seed for use in picking mirrors.  If HTTP_PROXY
837 # is set, this will be used to generate the seed; otherwise, the seed
838 # will be random.
839         if [ -n "${HTTP_PROXY}${http_proxy}" ]; then
840                 RANDVALUE=`sha256 -qs "${HTTP_PROXY}${http_proxy}" |
841                     tr -d 'a-f' |
842                     cut -c 1-9`
843         else
844                 RANDVALUE=`jot -r 1 0 999999999`
845         fi
846 }
847
848 # Pick a mirror.  Returns 1 if we have run out of mirrors to try.
849 fetch_pick_server () {
850 # Generate a list of not-yet-tried mirrors
851         sort serverlist_tried |
852             comm -23 serverlist_full - > serverlist
853
854 # Have we run out of mirrors?
855         if [ `wc -l < serverlist` -eq 0 ]; then
856                 echo "No mirrors remaining, giving up."
857                 return 1
858         fi
859
860 # Find the highest priority level (lowest numeric value).
861         SRV_PRIORITY=`cut -f 1 -d ' ' serverlist | sort -n | head -1`
862
863 # Add up the weights of the response lines at that priority level.
864         SRV_WSUM=0;
865         while read X; do
866                 case "$X" in
867                 ${SRV_PRIORITY}\ *)
868                         SRV_W=`echo $X | cut -f 2 -d ' '`
869                         SRV_WSUM=$(($SRV_WSUM + $SRV_W))
870                         ;;
871                 esac
872         done < serverlist
873
874 # If all the weights are 0, pretend that they are all 1 instead.
875         if [ ${SRV_WSUM} -eq 0 ]; then
876                 SRV_WSUM=`grep -E "^${SRV_PRIORITY} " serverlist | wc -l`
877                 SRV_W_ADD=1
878         else
879                 SRV_W_ADD=0
880         fi
881
882 # Pick a value between 0 and the sum of the weights - 1
883         SRV_RND=`expr ${RANDVALUE} % ${SRV_WSUM}`
884
885 # Read through the list of mirrors and set SERVERNAME.  Write the line
886 # corresponding to the mirror we selected into serverlist_tried so that
887 # we won't try it again.
888         while read X; do
889                 case "$X" in
890                 ${SRV_PRIORITY}\ *)
891                         SRV_W=`echo $X | cut -f 2 -d ' '`
892                         SRV_W=$(($SRV_W + $SRV_W_ADD))
893                         if [ $SRV_RND -lt $SRV_W ]; then
894                                 SERVERNAME=`echo $X | cut -f 3 -d ' '`
895                                 echo "$X" >> serverlist_tried
896                                 break
897                         else
898                                 SRV_RND=$(($SRV_RND - $SRV_W))
899                         fi
900                         ;;
901                 esac
902         done < serverlist
903 }
904
905 # Take a list of ${oldhash}|${newhash} and output a list of needed patches,
906 # i.e., those for which we have ${oldhash} and don't have ${newhash}.
907 fetch_make_patchlist () {
908         grep -vE "^([0-9a-f]{64})\|\1$" |
909             tr '|' ' ' |
910                 while read X Y; do
911                         if [ -f "files/${Y}.gz" ] ||
912                             [ ! -f "files/${X}.gz" ]; then
913                                 continue
914                         fi
915                         echo "${X}|${Y}"
916                 done | uniq
917 }
918
919 # Print user-friendly progress statistics
920 fetch_progress () {
921         LNC=0
922         while read x; do
923                 LNC=$(($LNC + 1))
924                 if [ $(($LNC % 10)) = 0 ]; then
925                         echo -n $LNC
926                 elif [ $(($LNC % 2)) = 0 ]; then
927                         echo -n .
928                 fi
929         done
930         echo -n " "
931 }
932
933 # Function for asking the user if everything is ok
934 continuep () {
935         while read -p "Does this look reasonable (y/n)? " CONTINUE; do
936                 case "${CONTINUE}" in
937                 y*)
938                         return 0
939                         ;;
940                 n*)
941                         return 1
942                         ;;
943                 esac
944         done
945 }
946
947 # Initialize the working directory
948 workdir_init () {
949         mkdir -p files
950         touch tINDEX.present
951 }
952
953 # Check that we have a public key with an appropriate hash, or
954 # fetch the key if it doesn't exist.  Returns 1 if the key has
955 # not yet been fetched.
956 fetch_key () {
957         if [ -r pub.ssl ] && [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
958                 return 0
959         fi
960
961         echo -n "Fetching public key from ${SERVERNAME}... "
962         rm -f pub.ssl
963         fetch ${QUIETFLAG} http://${SERVERNAME}/${FETCHDIR}/pub.ssl \
964             2>${QUIETREDIR} || true
965         if ! [ -r pub.ssl ]; then
966                 echo "failed."
967                 return 1
968         fi
969         if ! [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
970                 echo "key has incorrect hash."
971                 rm -f pub.ssl
972                 return 1
973         fi
974         echo "done."
975 }
976
977 # Fetch metadata signature, aka "tag".
978 fetch_tag () {
979         echo -n "Fetching metadata signature "
980         echo ${NDEBUG} "for ${RELNUM} from ${SERVERNAME}... "
981         rm -f latest.ssl
982         fetch ${QUIETFLAG} http://${SERVERNAME}/${FETCHDIR}/latest.ssl  \
983             2>${QUIETREDIR} || true
984         if ! [ -r latest.ssl ]; then
985                 echo "failed."
986                 return 1
987         fi
988
989         openssl rsautl -pubin -inkey pub.ssl -verify            \
990             < latest.ssl > tag.new 2>${QUIETREDIR} || true
991         rm latest.ssl
992
993         if ! [ `wc -l < tag.new` = 1 ] ||
994             ! grep -qE  \
995     "^freebsd-update\|${ARCH}\|${RELNUM}\|[0-9]+\|[0-9a-f]{64}\|[0-9]{10}" \
996                 tag.new; then
997                 echo "invalid signature."
998                 return 1
999         fi
1000
1001         echo "done."
1002
1003         RELPATCHNUM=`cut -f 4 -d '|' < tag.new`
1004         TINDEXHASH=`cut -f 5 -d '|' < tag.new`
1005         EOLTIME=`cut -f 6 -d '|' < tag.new`
1006 }
1007
1008 # Sanity-check the patch number in a tag, to make sure that we're not
1009 # going to "update" backwards and to prevent replay attacks.
1010 fetch_tagsanity () {
1011         # Check that we're not going to move from -pX to -pY with Y < X.
1012         RELPX=`uname -r | sed -E 's,.*-,,'`
1013         if echo ${RELPX} | grep -qE '^p[0-9]+$'; then
1014                 RELPX=`echo ${RELPX} | cut -c 2-`
1015         else
1016                 RELPX=0
1017         fi
1018         if [ "${RELPATCHNUM}" -lt "${RELPX}" ]; then
1019                 echo
1020                 echo -n "Files on mirror (${RELNUM}-p${RELPATCHNUM})"
1021                 echo " appear older than what"
1022                 echo "we are currently running (`uname -r`)!"
1023                 echo "Cowardly refusing to proceed any further."
1024                 return 1
1025         fi
1026
1027         # If "tag" exists and corresponds to ${RELNUM}, make sure that
1028         # it contains a patch number <= RELPATCHNUM, in order to protect
1029         # against rollback (replay) attacks.
1030         if [ -f tag ] &&
1031             grep -qE    \
1032     "^freebsd-update\|${ARCH}\|${RELNUM}\|[0-9]+\|[0-9a-f]{64}\|[0-9]{10}" \
1033                 tag; then
1034                 LASTRELPATCHNUM=`cut -f 4 -d '|' < tag`
1035
1036                 if [ "${RELPATCHNUM}" -lt "${LASTRELPATCHNUM}" ]; then
1037                         echo
1038                         echo -n "Files on mirror (${RELNUM}-p${RELPATCHNUM})"
1039                         echo " are older than the"
1040                         echo -n "most recently seen updates"
1041                         echo " (${RELNUM}-p${LASTRELPATCHNUM})."
1042                         echo "Cowardly refusing to proceed any further."
1043                         return 1
1044                 fi
1045         fi
1046 }
1047
1048 # Fetch metadata index file
1049 fetch_metadata_index () {
1050         echo ${NDEBUG} "Fetching metadata index... "
1051         rm -f ${TINDEXHASH}
1052         fetch ${QUIETFLAG} http://${SERVERNAME}/${FETCHDIR}/t/${TINDEXHASH}
1053             2>${QUIETREDIR}
1054         if ! [ -f ${TINDEXHASH} ]; then
1055                 echo "failed."
1056                 return 1
1057         fi
1058         if [ `${SHA256} -q ${TINDEXHASH}` != ${TINDEXHASH} ]; then
1059                 echo "update metadata index corrupt."
1060                 return 1
1061         fi
1062         echo "done."
1063 }
1064
1065 # Print an error message about signed metadata being bogus.
1066 fetch_metadata_bogus () {
1067         echo
1068         echo "The update metadata$1 is correctly signed, but"
1069         echo "failed an integrity check."
1070         echo "Cowardly refusing to proceed any further."
1071         return 1
1072 }
1073
1074 # Construct tINDEX.new by merging the lines named in $1 from ${TINDEXHASH}
1075 # with the lines not named in $@ from tINDEX.present (if that file exists).
1076 fetch_metadata_index_merge () {
1077         for METAFILE in $@; do
1078                 if [ `grep -E "^${METAFILE}\|" ${TINDEXHASH} | wc -l`   \
1079                     -ne 1 ]; then
1080                         fetch_metadata_bogus " index"
1081                         return 1
1082                 fi
1083
1084                 grep -E "${METAFILE}\|" ${TINDEXHASH}
1085         done |
1086             sort > tINDEX.wanted
1087
1088         if [ -f tINDEX.present ]; then
1089                 join -t '|' -v 2 tINDEX.wanted tINDEX.present |
1090                     sort -m - tINDEX.wanted > tINDEX.new
1091                 rm tINDEX.wanted
1092         else
1093                 mv tINDEX.wanted tINDEX.new
1094         fi
1095 }
1096
1097 # Sanity check all the lines of tINDEX.new.  Even if more metadata lines
1098 # are added by future versions of the server, this won't cause problems,
1099 # since the only lines which appear in tINDEX.new are the ones which we
1100 # specifically grepped out of ${TINDEXHASH}.
1101 fetch_metadata_index_sanity () {
1102         if grep -qvE '^[0-9A-Z.-]+\|[0-9a-f]{64}$' tINDEX.new; then
1103                 fetch_metadata_bogus " index"
1104                 return 1
1105         fi
1106 }
1107
1108 # Sanity check the metadata file $1.
1109 fetch_metadata_sanity () {
1110         # Some aliases to save space later: ${P} is a character which can
1111         # appear in a path; ${M} is the four numeric metadata fields; and
1112         # ${H} is a sha256 hash.
1113         P="[-+./:=_[[:alnum:]]"
1114         M="[0-9]+\|[0-9]+\|[0-9]+\|[0-9]+"
1115         H="[0-9a-f]{64}"
1116
1117         # Check that the first four fields make sense.
1118         if gunzip -c < files/$1.gz |
1119             grep -qvE "^[a-z]+\|[0-9a-z]+\|${P}+\|[fdL-]\|"; then
1120                 fetch_metadata_bogus ""
1121                 return 1
1122         fi
1123
1124         # Remove the first three fields.
1125         gunzip -c < files/$1.gz |
1126             cut -f 4- -d '|' > sanitycheck.tmp
1127
1128         # Sanity check entries with type 'f'
1129         if grep -E '^f' sanitycheck.tmp |
1130             grep -qvE "^f\|${M}\|${H}\|${P}*\$"; then
1131                 fetch_metadata_bogus ""
1132                 return 1
1133         fi
1134
1135         # Sanity check entries with type 'd'
1136         if grep -E '^d' sanitycheck.tmp |
1137             grep -qvE "^d\|${M}\|\|\$"; then
1138                 fetch_metadata_bogus ""
1139                 return 1
1140         fi
1141
1142         # Sanity check entries with type 'L'
1143         if grep -E '^L' sanitycheck.tmp |
1144             grep -qvE "^L\|${M}\|${P}*\|\$"; then
1145                 fetch_metadata_bogus ""
1146                 return 1
1147         fi
1148
1149         # Sanity check entries with type '-'
1150         if grep -E '^-' sanitycheck.tmp |
1151             grep -qvE "^-\|\|\|\|\|\|"; then
1152                 fetch_metadata_bogus ""
1153                 return 1
1154         fi
1155
1156         # Clean up
1157         rm sanitycheck.tmp
1158 }
1159
1160 # Fetch the metadata index and metadata files listed in $@,
1161 # taking advantage of metadata patches where possible.
1162 fetch_metadata () {
1163         fetch_metadata_index || return 1
1164         fetch_metadata_index_merge $@ || return 1
1165         fetch_metadata_index_sanity || return 1
1166
1167         # Generate a list of wanted metadata patches
1168         join -t '|' -o 1.2,2.2 tINDEX.present tINDEX.new |
1169             fetch_make_patchlist > patchlist
1170
1171         if [ -s patchlist ]; then
1172                 # Attempt to fetch metadata patches
1173                 echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
1174                 echo ${NDEBUG} "metadata patches.${DDSTATS}"
1175                 tr '|' '-' < patchlist |
1176                     lam -s "${FETCHDIR}/tp/" - -s ".gz" |
1177                     xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
1178                         2>${STATSREDIR} | fetch_progress
1179                 echo "done."
1180
1181                 # Attempt to apply metadata patches
1182                 echo -n "Applying metadata patches... "
1183                 tr '|' ' ' < patchlist |
1184                     while read X Y; do
1185                         if [ ! -f "${X}-${Y}.gz" ]; then continue; fi
1186                         gunzip -c < ${X}-${Y}.gz > diff
1187                         gunzip -c < files/${X}.gz > diff-OLD
1188
1189                         # Figure out which lines are being added and removed
1190                         grep -E '^-' diff |
1191                             cut -c 2- |
1192                             while read PREFIX; do
1193                                 look "${PREFIX}" diff-OLD
1194                             done |
1195                             sort > diff-rm
1196                         grep -E '^\+' diff |
1197                             cut -c 2- > diff-add
1198
1199                         # Generate the new file
1200                         comm -23 diff-OLD diff-rm |
1201                             sort - diff-add > diff-NEW
1202
1203                         if [ `${SHA256} -q diff-NEW` = ${Y} ]; then
1204                                 mv diff-NEW files/${Y}
1205                                 gzip -n files/${Y}
1206                         else
1207                                 mv diff-NEW ${Y}.bad
1208                         fi
1209                         rm -f ${X}-${Y}.gz diff
1210                         rm -f diff-OLD diff-NEW diff-add diff-rm
1211                 done 2>${QUIETREDIR}
1212                 echo "done."
1213         fi
1214
1215         # Update metadata without patches
1216         cut -f 2 -d '|' < tINDEX.new |
1217             while read Y; do
1218                 if [ ! -f "files/${Y}.gz" ]; then
1219                         echo ${Y};
1220                 fi
1221             done |
1222             sort -u > filelist
1223
1224         if [ -s filelist ]; then
1225                 echo -n "Fetching `wc -l < filelist | tr -d ' '` "
1226                 echo ${NDEBUG} "metadata files... "
1227                 lam -s "${FETCHDIR}/m/" - -s ".gz" < filelist |
1228                     xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
1229                     2>${QUIETREDIR}
1230
1231                 while read Y; do
1232                         if ! [ -f ${Y}.gz ]; then
1233                                 echo "failed."
1234                                 return 1
1235                         fi
1236                         if [ `gunzip -c < ${Y}.gz |
1237                             ${SHA256} -q` = ${Y} ]; then
1238                                 mv ${Y}.gz files/${Y}.gz
1239                         else
1240                                 echo "metadata is corrupt."
1241                                 return 1
1242                         fi
1243                 done < filelist
1244                 echo "done."
1245         fi
1246
1247 # Sanity-check the metadata files.
1248         cut -f 2 -d '|' tINDEX.new > filelist
1249         while read X; do
1250                 fetch_metadata_sanity ${X} || return 1
1251         done < filelist
1252
1253 # Remove files which are no longer needed
1254         cut -f 2 -d '|' tINDEX.present |
1255             sort > oldfiles
1256         cut -f 2 -d '|' tINDEX.new |
1257             sort |
1258             comm -13 - oldfiles |
1259             lam -s "files/" - -s ".gz" |
1260             xargs rm -f
1261         rm patchlist filelist oldfiles
1262         rm ${TINDEXHASH}
1263
1264 # We're done!
1265         mv tINDEX.new tINDEX.present
1266         mv tag.new tag
1267
1268         return 0
1269 }
1270
1271 # Extract a subset of a downloaded metadata file containing only the parts
1272 # which are listed in COMPONENTS.
1273 fetch_filter_metadata_components () {
1274         METAHASH=`look "$1|" tINDEX.present | cut -f 2 -d '|'`
1275         gunzip -c < files/${METAHASH}.gz > $1.all
1276
1277         # Fish out the lines belonging to components we care about.
1278         for C in ${COMPONENTS}; do
1279                 look "`echo ${C} | tr '/' '|'`|" $1.all
1280         done > $1
1281
1282         # Remove temporary file.
1283         rm $1.all
1284 }
1285
1286 # Generate a filtered version of the metadata file $1 from the downloaded
1287 # file, by fishing out the lines corresponding to components we're trying
1288 # to keep updated, and then removing lines corresponding to paths we want
1289 # to ignore.
1290 fetch_filter_metadata () {
1291         # Fish out the lines belonging to components we care about.
1292         fetch_filter_metadata_components $1
1293
1294         # Canonicalize directory names by removing any trailing / in
1295         # order to avoid listing directories multiple times if they
1296         # belong to multiple components.  Turning "/" into "" doesn't
1297         # matter, since we add a leading "/" when we use paths later.
1298         cut -f 3- -d '|' $1 |
1299             sed -e 's,/|d|,|d|,' |
1300             sort -u > $1.tmp
1301
1302         # Figure out which lines to ignore and remove them.
1303         for X in ${IGNOREPATHS}; do
1304                 grep -E "^${X}" $1.tmp
1305         done |
1306             sort -u |
1307             comm -13 - $1.tmp > $1
1308
1309         # Remove temporary files.
1310         rm $1.tmp
1311 }
1312
1313 # Filter the metadata file $1 by adding lines with "/boot/$2"
1314 # replaced by ${KERNELDIR} (which is `sysctl -n kern.bootfile` minus the
1315 # trailing "/kernel"); and if "/boot/$2" does not exist, remove
1316 # the original lines which start with that.
1317 # Put another way: Deal with the fact that the FOO kernel is sometimes
1318 # installed in /boot/FOO/ and is sometimes installed elsewhere.
1319 fetch_filter_kernel_names () {
1320         grep ^/boot/$2 $1 |
1321             sed -e "s,/boot/$2,${KERNELDIR},g" |
1322             sort - $1 > $1.tmp
1323         mv $1.tmp $1
1324
1325         if ! [ -d /boot/$2 ]; then
1326                 grep -v ^/boot/$2 $1 > $1.tmp
1327                 mv $1.tmp $1
1328         fi
1329 }
1330
1331 # For all paths appearing in $1 or $3, inspect the system
1332 # and generate $2 describing what is currently installed.
1333 fetch_inspect_system () {
1334         # No errors yet...
1335         rm -f .err
1336
1337         # Tell the user why his disk is suddenly making lots of noise
1338         echo -n "Inspecting system... "
1339
1340         # Generate list of files to inspect
1341         cat $1 $3 |
1342             cut -f 1 -d '|' |
1343             sort -u > filelist
1344
1345         # Examine each file and output lines of the form
1346         # /path/to/file|type|device-inum|user|group|perm|flags|value
1347         # sorted by device and inode number.
1348         while read F; do
1349                 # If the symlink/file/directory does not exist, record this.
1350                 if ! [ -e ${BASEDIR}/${F} ]; then
1351                         echo "${F}|-||||||"
1352                         continue
1353                 fi
1354                 if ! [ -r ${BASEDIR}/${F} ]; then
1355                         echo "Cannot read file: ${BASEDIR}/${F}"        \
1356                             >/dev/stderr
1357                         touch .err
1358                         return 1
1359                 fi
1360
1361                 # Otherwise, output an index line.
1362                 if [ -L ${BASEDIR}/${F} ]; then
1363                         echo -n "${F}|L|"
1364                         stat -n -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
1365                         readlink ${BASEDIR}/${F};
1366                 elif [ -f ${BASEDIR}/${F} ]; then
1367                         echo -n "${F}|f|"
1368                         stat -n -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
1369                         sha256 -q ${BASEDIR}/${F};
1370                 elif [ -d ${BASEDIR}/${F} ]; then
1371                         echo -n "${F}|d|"
1372                         stat -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
1373                 else
1374                         echo "Unknown file type: ${BASEDIR}/${F}"       \
1375                             >/dev/stderr
1376                         touch .err
1377                         return 1
1378                 fi
1379         done < filelist |
1380             sort -k 3,3 -t '|' > $2.tmp
1381         rm filelist
1382
1383         # Check if an error occured during system inspection
1384         if [ -f .err ]; then
1385                 return 1
1386         fi
1387
1388         # Convert to the form
1389         # /path/to/file|type|user|group|perm|flags|value|hlink
1390         # by resolving identical device and inode numbers into hard links.
1391         cut -f 1,3 -d '|' $2.tmp |
1392             sort -k 1,1 -t '|' |
1393             sort -s -u -k 2,2 -t '|' |
1394             join -1 2 -2 3 -t '|' - $2.tmp |
1395             awk -F \| -v OFS=\|         \
1396                 '{
1397                     if (($2 == $3) || ($4 == "-"))
1398                         print $3,$4,$5,$6,$7,$8,$9,""
1399                     else
1400                         print $3,$4,$5,$6,$7,$8,$9,$2
1401                 }' |
1402             sort > $2
1403         rm $2.tmp
1404
1405         # We're finished looking around
1406         echo "done."
1407 }
1408
1409 # For any paths matching ${MERGECHANGES}, compare $1 and $2 and find any
1410 # files which differ; generate $3 containing these paths and the old hashes.
1411 fetch_filter_mergechanges () {
1412         # Pull out the paths and hashes of the files matching ${MERGECHANGES}.
1413         for F in $1 $2; do
1414                 for X in ${MERGECHANGES}; do
1415                         grep -E "^${X}" ${F}
1416                 done |
1417                     cut -f 1,2,7 -d '|' |
1418                     sort > ${F}-values
1419         done
1420
1421         # Any line in $2-values which doesn't appear in $1-values and is a
1422         # file means that we should list the path in $3.
1423         comm -13 $1-values $2-values |
1424             fgrep '|f|' |
1425             cut -f 1 -d '|' > $2-paths
1426
1427         # For each path, pull out one (and only one!) entry from $1-values.
1428         # Note that we cannot distinguish which "old" version the user made
1429         # changes to; but hopefully any changes which occur due to security
1430         # updates will exist in both the "new" version and the version which
1431         # the user has installed, so the merging will still work.
1432         while read X; do
1433                 look "${X}|" $1-values |
1434                     head -1
1435         done < $2-paths > $3
1436
1437         # Clean up
1438         rm $1-values $2-values $2-paths
1439 }
1440
1441 # For any paths matching ${UPDATEIFUNMODIFIED}, remove lines from $[123]
1442 # which correspond to lines in $2 with hashes not matching $1 or $3, unless
1443 # the paths are listed in $4.  For entries in $2 marked "not present"
1444 # (aka. type -), remove lines from $[123] unless there is a corresponding
1445 # entry in $1.
1446 fetch_filter_unmodified_notpresent () {
1447         # Figure out which lines of $1 and $3 correspond to bits which
1448         # should only be updated if they haven't changed, and fish out
1449         # the (path, type, value) tuples.
1450         # NOTE: We don't consider a file to be "modified" if it matches
1451         # the hash from $3.
1452         for X in ${UPDATEIFUNMODIFIED}; do
1453                 grep -E "^${X}" $1
1454                 grep -E "^${X}" $3
1455         done |
1456             cut -f 1,2,7 -d '|' |
1457             sort > $1-values
1458
1459         # Do the same for $2.
1460         for X in ${UPDATEIFUNMODIFIED}; do
1461                 grep -E "^${X}" $2
1462         done |
1463             cut -f 1,2,7 -d '|' |
1464             sort > $2-values
1465
1466         # Any entry in $2-values which is not in $1-values corresponds to
1467         # a path which we need to remove from $1, $2, and $3, unless it
1468         # that path appears in $4.
1469         comm -13 $1-values $2-values |
1470             sort -t '|' -k 1,1 > mlines.tmp
1471         cut -f 1 -d '|' $4 |
1472             sort |
1473             join -v 2 -t '|' - mlines.tmp |
1474             sort > mlines
1475         rm $1-values $2-values mlines.tmp
1476
1477         # Any lines in $2 which are not in $1 AND are "not present" lines
1478         # also belong in mlines.
1479         comm -13 $1 $2 |
1480             cut -f 1,2,7 -d '|' |
1481             fgrep '|-|' >> mlines
1482
1483         # Remove lines from $1, $2, and $3
1484         for X in $1 $2 $3; do
1485                 sort -t '|' -k 1,1 ${X} > ${X}.tmp
1486                 cut -f 1 -d '|' < mlines |
1487                     sort |
1488                     join -v 2 -t '|' - ${X}.tmp |
1489                     sort > ${X}
1490                 rm ${X}.tmp
1491         done
1492
1493         # Store a list of the modified files, for future reference
1494         fgrep -v '|-|' mlines |
1495             cut -f 1 -d '|' > modifiedfiles
1496         rm mlines
1497 }
1498
1499 # For each entry in $1 of type -, remove any corresponding
1500 # entry from $2 if ${ALLOWADD} != "yes".  Remove all entries
1501 # of type - from $1.
1502 fetch_filter_allowadd () {
1503         cut -f 1,2 -d '|' < $1 |
1504             fgrep '|-' |
1505             cut -f 1 -d '|' > filesnotpresent
1506
1507         if [ ${ALLOWADD} != "yes" ]; then
1508                 sort < $2 |
1509                     join -v 1 -t '|' - filesnotpresent |
1510                     sort > $2.tmp
1511                 mv $2.tmp $2
1512         fi
1513
1514         sort < $1 |
1515             join -v 1 -t '|' - filesnotpresent |
1516             sort > $1.tmp
1517         mv $1.tmp $1
1518         rm filesnotpresent
1519 }
1520
1521 # If ${ALLOWDELETE} != "yes", then remove any entries from $1
1522 # which don't correspond to entries in $2.
1523 fetch_filter_allowdelete () {
1524         # Produce a lists ${PATH}|${TYPE}
1525         for X in $1 $2; do
1526                 cut -f 1-2 -d '|' < ${X} |
1527                     sort -u > ${X}.nodes
1528         done
1529
1530         # Figure out which lines need to be removed from $1.
1531         if [ ${ALLOWDELETE} != "yes" ]; then
1532                 comm -23 $1.nodes $2.nodes > $1.badnodes
1533         else
1534                 : > $1.badnodes
1535         fi
1536
1537         # Remove the relevant lines from $1
1538         while read X; do
1539                 look "${X}|" $1
1540         done < $1.badnodes |
1541             comm -13 - $1 > $1.tmp
1542         mv $1.tmp $1
1543
1544         rm $1.badnodes $1.nodes $2.nodes
1545 }
1546
1547 # If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in $2
1548 # with metadata not matching any entry in $1, replace the corresponding
1549 # line of $3 with one having the same metadata as the entry in $2.
1550 fetch_filter_modified_metadata () {
1551         # Fish out the metadata from $1 and $2
1552         for X in $1 $2; do
1553                 cut -f 1-6 -d '|' < ${X} > ${X}.metadata
1554         done
1555
1556         # Find the metadata we need to keep
1557         if [ ${KEEPMODIFIEDMETADATA} = "yes" ]; then
1558                 comm -13 $1.metadata $2.metadata > keepmeta
1559         else
1560                 : > keepmeta
1561         fi
1562
1563         # Extract the lines which we need to remove from $3, and
1564         # construct the lines which we need to add to $3.
1565         : > $3.remove
1566         : > $3.add
1567         while read LINE; do
1568                 NODE=`echo "${LINE}" | cut -f 1-2 -d '|'`
1569                 look "${NODE}|" $3 >> $3.remove
1570                 look "${NODE}|" $3 |
1571                     cut -f 7- -d '|' |
1572                     lam -s "${LINE}|" - >> $3.add
1573         done < keepmeta
1574
1575         # Remove the specified lines and add the new lines.
1576         sort $3.remove |
1577             comm -13 - $3 |
1578             sort -u - $3.add > $3.tmp
1579         mv $3.tmp $3
1580
1581         rm keepmeta $1.metadata $2.metadata $3.add $3.remove
1582 }
1583
1584 # Remove lines from $1 and $2 which are identical;
1585 # no need to update a file if it isn't changing.
1586 fetch_filter_uptodate () {
1587         comm -23 $1 $2 > $1.tmp
1588         comm -13 $1 $2 > $2.tmp
1589
1590         mv $1.tmp $1
1591         mv $2.tmp $2
1592 }
1593
1594 # Fetch any "clean" old versions of files we need for merging changes.
1595 fetch_files_premerge () {
1596         # We only need to do anything if $1 is non-empty.
1597         if [ -s $1 ]; then
1598                 # Tell the user what we're doing
1599                 echo -n "Fetching files from ${OLDRELNUM} for merging... "
1600
1601                 # List of files wanted
1602                 fgrep '|f|' < $1 |
1603                     cut -f 3 -d '|' |
1604                     sort -u > files.wanted
1605
1606                 # Only fetch the files we don't already have
1607                 while read Y; do
1608                         if [ ! -f "files/${Y}.gz" ]; then
1609                                 echo ${Y};
1610                         fi
1611                 done < files.wanted > filelist
1612
1613                 # Actually fetch them
1614                 lam -s "${OLDFETCHDIR}/f/" - -s ".gz" < filelist |
1615                     xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
1616                     2>${QUIETREDIR}
1617
1618                 # Make sure we got them all, and move them into /files/
1619                 while read Y; do
1620                         if ! [ -f ${Y}.gz ]; then
1621                                 echo "failed."
1622                                 return 1
1623                         fi
1624                         if [ `gunzip -c < ${Y}.gz |
1625                             ${SHA256} -q` = ${Y} ]; then
1626                                 mv ${Y}.gz files/${Y}.gz
1627                         else
1628                                 echo "${Y} has incorrect hash."
1629                                 return 1
1630                         fi
1631                 done < filelist
1632                 echo "done."
1633
1634                 # Clean up
1635                 rm filelist files.wanted
1636         fi
1637 }
1638
1639 # Prepare to fetch files: Generate a list of the files we need,
1640 # copy the unmodified files we have into /files/, and generate
1641 # a list of patches to download.
1642 fetch_files_prepare () {
1643         # Tell the user why his disk is suddenly making lots of noise
1644         echo -n "Preparing to download files... "
1645
1646         # Reduce indices to ${PATH}|${HASH} pairs
1647         for X in $1 $2 $3; do
1648                 cut -f 1,2,7 -d '|' < ${X} |
1649                     fgrep '|f|' |
1650                     cut -f 1,3 -d '|' |
1651                     sort > ${X}.hashes
1652         done
1653
1654         # List of files wanted
1655         cut -f 2 -d '|' < $3.hashes |
1656             sort -u |
1657             while read HASH; do
1658                 if ! [ -f files/${HASH}.gz ]; then
1659                         echo ${HASH}
1660                 fi
1661         done > files.wanted
1662
1663         # Generate a list of unmodified files
1664         comm -12 $1.hashes $2.hashes |
1665             sort -k 1,1 -t '|' > unmodified.files
1666
1667         # Copy all files into /files/.  We only need the unmodified files
1668         # for use in patching; but we'll want all of them if the user asks
1669         # to rollback the updates later.
1670         while read LINE; do
1671                 F=`echo "${LINE}" | cut -f 1 -d '|'`
1672                 HASH=`echo "${LINE}" | cut -f 2 -d '|'`
1673
1674                 # Skip files we already have.
1675                 if [ -f files/${HASH}.gz ]; then
1676                         continue
1677                 fi
1678
1679                 # Make sure the file hasn't changed.
1680                 cp "${BASEDIR}/${F}" tmpfile
1681                 if [ `sha256 -q tmpfile` != ${HASH} ]; then
1682                         echo
1683                         echo "File changed while FreeBSD Update running: ${F}"
1684                         return 1
1685                 fi
1686
1687                 # Place the file into storage.
1688                 gzip -c < tmpfile > files/${HASH}.gz
1689                 rm tmpfile
1690         done < $2.hashes
1691
1692         # Produce a list of patches to download
1693         sort -k 1,1 -t '|' $3.hashes |
1694             join -t '|' -o 2.2,1.2 - unmodified.files |
1695             fetch_make_patchlist > patchlist
1696
1697         # Garbage collect
1698         rm unmodified.files $1.hashes $2.hashes $3.hashes
1699
1700         # We don't need the list of possible old files any more.
1701         rm $1
1702
1703         # We're finished making noise
1704         echo "done."
1705 }
1706
1707 # Fetch files.
1708 fetch_files () {
1709         # Attempt to fetch patches
1710         if [ -s patchlist ]; then
1711                 echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
1712                 echo ${NDEBUG} "patches.${DDSTATS}"
1713                 tr '|' '-' < patchlist |
1714                     lam -s "${PATCHDIR}/" - |
1715                     xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
1716                         2>${STATSREDIR} | fetch_progress
1717                 echo "done."
1718
1719                 # Attempt to apply patches
1720                 echo -n "Applying patches... "
1721                 tr '|' ' ' < patchlist |
1722                     while read X Y; do
1723                         if [ ! -f "${X}-${Y}" ]; then continue; fi
1724                         gunzip -c < files/${X}.gz > OLD
1725
1726                         bspatch OLD NEW ${X}-${Y}
1727
1728                         if [ `${SHA256} -q NEW` = ${Y} ]; then
1729                                 mv NEW files/${Y}
1730                                 gzip -n files/${Y}
1731                         fi
1732                         rm -f diff OLD NEW ${X}-${Y}
1733                 done 2>${QUIETREDIR}
1734                 echo "done."
1735         fi
1736
1737         # Download files which couldn't be generate via patching
1738         while read Y; do
1739                 if [ ! -f "files/${Y}.gz" ]; then
1740                         echo ${Y};
1741                 fi
1742         done < files.wanted > filelist
1743
1744         if [ -s filelist ]; then
1745                 echo -n "Fetching `wc -l < filelist | tr -d ' '` "
1746                 echo ${NDEBUG} "files... "
1747                 lam -s "${FETCHDIR}/f/" - -s ".gz" < filelist |
1748                     xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
1749                     2>${QUIETREDIR}
1750
1751                 while read Y; do
1752                         if ! [ -f ${Y}.gz ]; then
1753                                 echo "failed."
1754                                 return 1
1755                         fi
1756                         if [ `gunzip -c < ${Y}.gz |
1757                             ${SHA256} -q` = ${Y} ]; then
1758                                 mv ${Y}.gz files/${Y}.gz
1759                         else
1760                                 echo "${Y} has incorrect hash."
1761                                 return 1
1762                         fi
1763                 done < filelist
1764                 echo "done."
1765         fi
1766
1767         # Clean up
1768         rm files.wanted filelist patchlist
1769 }
1770
1771 # Create and populate install manifest directory; and report what updates
1772 # are available.
1773 fetch_create_manifest () {
1774         # If we have an existing install manifest, nuke it.
1775         if [ -L "${BDHASH}-install" ]; then
1776                 rm -r ${BDHASH}-install/
1777                 rm ${BDHASH}-install
1778         fi
1779
1780         # Report to the user if any updates were avoided due to local changes
1781         if [ -s modifiedfiles ]; then
1782                 echo
1783                 echo -n "The following files are affected by updates, "
1784                 echo "but no changes have"
1785                 echo -n "been downloaded because the files have been "
1786                 echo "modified locally:"
1787                 cat modifiedfiles
1788         fi | more
1789         rm modifiedfiles
1790
1791         # If no files will be updated, tell the user and exit
1792         if ! [ -s INDEX-PRESENT ] &&
1793             ! [ -s INDEX-NEW ]; then
1794                 rm INDEX-PRESENT INDEX-NEW
1795                 echo
1796                 echo -n "No updates needed to update system to "
1797                 echo "${RELNUM}-p${RELPATCHNUM}."
1798                 return
1799         fi
1800
1801         # Divide files into (a) removed files, (b) added files, and
1802         # (c) updated files.
1803         cut -f 1 -d '|' < INDEX-PRESENT |
1804             sort > INDEX-PRESENT.flist
1805         cut -f 1 -d '|' < INDEX-NEW |
1806             sort > INDEX-NEW.flist
1807         comm -23 INDEX-PRESENT.flist INDEX-NEW.flist > files.removed
1808         comm -13 INDEX-PRESENT.flist INDEX-NEW.flist > files.added
1809         comm -12 INDEX-PRESENT.flist INDEX-NEW.flist > files.updated
1810         rm INDEX-PRESENT.flist INDEX-NEW.flist
1811
1812         # Report removed files, if any
1813         if [ -s files.removed ]; then
1814                 echo
1815                 echo -n "The following files will be removed "
1816                 echo "as part of updating to ${RELNUM}-p${RELPATCHNUM}:"
1817                 cat files.removed
1818         fi | more
1819         rm files.removed
1820
1821         # Report added files, if any
1822         if [ -s files.added ]; then
1823                 echo
1824                 echo -n "The following files will be added "
1825                 echo "as part of updating to ${RELNUM}-p${RELPATCHNUM}:"
1826                 cat files.added
1827         fi | more
1828         rm files.added
1829
1830         # Report updated files, if any
1831         if [ -s files.updated ]; then
1832                 echo
1833                 echo -n "The following files will be updated "
1834                 echo "as part of updating to ${RELNUM}-p${RELPATCHNUM}:"
1835
1836                 cat files.updated
1837         fi | more
1838         rm files.updated
1839
1840         # Create a directory for the install manifest.
1841         MDIR=`mktemp -d install.XXXXXX` || return 1
1842
1843         # Populate it
1844         mv INDEX-PRESENT ${MDIR}/INDEX-OLD
1845         mv INDEX-NEW ${MDIR}/INDEX-NEW
1846
1847         # Link it into place
1848         ln -s ${MDIR} ${BDHASH}-install
1849 }
1850
1851 # Warn about any upcoming EoL
1852 fetch_warn_eol () {
1853         # What's the current time?
1854         NOWTIME=`date "+%s"`
1855
1856         # When did we last warn about the EoL date?
1857         if [ -f lasteolwarn ]; then
1858                 LASTWARN=`cat lasteolwarn`
1859         else
1860                 LASTWARN=`expr ${NOWTIME} - 63072000`
1861         fi
1862
1863         # If the EoL time is past, warn.
1864         if [ ${EOLTIME} -lt ${NOWTIME} ]; then
1865                 echo
1866                 cat <<-EOF
1867                 WARNING: `uname -sr` HAS PASSED ITS END-OF-LIFE DATE.
1868                 Any security issues discovered after `date -r ${EOLTIME}`
1869                 will not have been corrected.
1870                 EOF
1871                 return 1
1872         fi
1873
1874         # Figure out how long it has been since we last warned about the
1875         # upcoming EoL, and how much longer we have left.
1876         SINCEWARN=`expr ${NOWTIME} - ${LASTWARN}`
1877         TIMELEFT=`expr ${EOLTIME} - ${NOWTIME}`
1878
1879         # Don't warn if the EoL is more than 3 months away
1880         if [ ${TIMELEFT} -gt 7884000 ]; then
1881                 return 0
1882         fi
1883
1884         # Don't warn if the time remaining is more than 3 times the time
1885         # since the last warning.
1886         if [ ${TIMELEFT} -gt `expr ${SINCEWARN} \* 3` ]; then
1887                 return 0
1888         fi
1889
1890         # Figure out what time units to use.
1891         if [ ${TIMELEFT} -lt 604800 ]; then
1892                 UNIT="day"
1893                 SIZE=86400
1894         elif [ ${TIMELEFT} -lt 2678400 ]; then
1895                 UNIT="week"
1896                 SIZE=604800
1897         else
1898                 UNIT="month"
1899                 SIZE=2678400
1900         fi
1901
1902         # Compute the right number of units
1903         NUM=`expr ${TIMELEFT} / ${SIZE}`
1904         if [ ${NUM} != 1 ]; then
1905                 UNIT="${UNIT}s"
1906         fi
1907
1908         # Print the warning
1909         echo
1910         cat <<-EOF
1911                 WARNING: `uname -sr` is approaching its End-of-Life date.
1912                 It is strongly recommended that you upgrade to a newer
1913                 release within the next ${NUM} ${UNIT}.
1914         EOF
1915
1916         # Update the stored time of last warning
1917         echo ${NOWTIME} > lasteolwarn
1918 }
1919
1920 # Do the actual work involved in "fetch" / "cron".
1921 fetch_run () {
1922         workdir_init || return 1
1923
1924         # Prepare the mirror list.
1925         fetch_pick_server_init && fetch_pick_server
1926
1927         # Try to fetch the public key until we run out of servers.
1928         while ! fetch_key; do
1929                 fetch_pick_server || return 1
1930         done
1931
1932         # Try to fetch the metadata index signature ("tag") until we run
1933         # out of available servers; and sanity check the downloaded tag.
1934         while ! fetch_tag; do
1935                 fetch_pick_server || return 1
1936         done
1937         fetch_tagsanity || return 1
1938
1939         # Fetch the latest INDEX-NEW and INDEX-OLD files.
1940         fetch_metadata INDEX-NEW INDEX-OLD || return 1
1941
1942         # Generate filtered INDEX-NEW and INDEX-OLD files containing only
1943         # the lines which (a) belong to components we care about, and (b)
1944         # don't correspond to paths we're explicitly ignoring.
1945         fetch_filter_metadata INDEX-NEW || return 1
1946         fetch_filter_metadata INDEX-OLD || return 1
1947
1948         # Translate /boot/${KERNCONF} into ${KERNELDIR}
1949         fetch_filter_kernel_names INDEX-NEW ${KERNCONF}
1950         fetch_filter_kernel_names INDEX-OLD ${KERNCONF}
1951
1952         # For all paths appearing in INDEX-OLD or INDEX-NEW, inspect the
1953         # system and generate an INDEX-PRESENT file.
1954         fetch_inspect_system INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1
1955
1956         # Based on ${UPDATEIFUNMODIFIED}, remove lines from INDEX-* which
1957         # correspond to lines in INDEX-PRESENT with hashes not appearing
1958         # in INDEX-OLD or INDEX-NEW.  Also remove lines where the entry in
1959         # INDEX-PRESENT has type - and there isn't a corresponding entry in
1960         # INDEX-OLD with type -.
1961         fetch_filter_unmodified_notpresent      \
1962             INDEX-OLD INDEX-PRESENT INDEX-NEW /dev/null
1963
1964         # For each entry in INDEX-PRESENT of type -, remove any corresponding
1965         # entry from INDEX-NEW if ${ALLOWADD} != "yes".  Remove all entries
1966         # of type - from INDEX-PRESENT.
1967         fetch_filter_allowadd INDEX-PRESENT INDEX-NEW
1968
1969         # If ${ALLOWDELETE} != "yes", then remove any entries from
1970         # INDEX-PRESENT which don't correspond to entries in INDEX-NEW.
1971         fetch_filter_allowdelete INDEX-PRESENT INDEX-NEW
1972
1973         # If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in
1974         # INDEX-PRESENT with metadata not matching any entry in INDEX-OLD,
1975         # replace the corresponding line of INDEX-NEW with one having the
1976         # same metadata as the entry in INDEX-PRESENT.
1977         fetch_filter_modified_metadata INDEX-OLD INDEX-PRESENT INDEX-NEW
1978
1979         # Remove lines from INDEX-PRESENT and INDEX-NEW which are identical;
1980         # no need to update a file if it isn't changing.
1981         fetch_filter_uptodate INDEX-PRESENT INDEX-NEW
1982
1983         # Prepare to fetch files: Generate a list of the files we need,
1984         # copy the unmodified files we have into /files/, and generate
1985         # a list of patches to download.
1986         fetch_files_prepare INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1
1987
1988         # Fetch files.
1989         fetch_files || return 1
1990
1991         # Create and populate install manifest directory; and report what
1992         # updates are available.
1993         fetch_create_manifest || return 1
1994
1995         # Warn about any upcoming EoL
1996         fetch_warn_eol || return 1
1997 }
1998
1999 # If StrictComponents is not "yes", generate a new components list
2000 # with only the components which appear to be installed.
2001 upgrade_guess_components () {
2002         if [ "${STRICTCOMPONENTS}" = "no" ]; then
2003                 # Generate filtered INDEX-ALL with only the components listed
2004                 # in COMPONENTS.
2005                 fetch_filter_metadata_components $1 || return 1
2006
2007                 # Tell the user why his disk is suddenly making lots of noise
2008                 echo -n "Inspecting system... "
2009
2010                 # Look at the files on disk, and assume that a component is
2011                 # supposed to be present if it is more than half-present.
2012                 cut -f 1-3 -d '|' < INDEX-ALL |
2013                     tr '|' ' ' |
2014                     while read C S F; do
2015                         if [ -e ${BASEDIR}/${F} ]; then
2016                                 echo "+ ${C}|${S}"
2017                         fi
2018                         echo "= ${C}|${S}"
2019                     done |
2020                     sort |
2021                     uniq -c |
2022                     sed -E 's,^ +,,' > compfreq
2023                 grep ' = ' compfreq |
2024                     cut -f 1,3 -d ' ' |
2025                     sort -k 2,2 -t ' ' > compfreq.total
2026                 grep ' + ' compfreq |
2027                     cut -f 1,3 -d ' ' |
2028                     sort -k 2,2 -t ' ' > compfreq.present
2029                 join -t ' ' -1 2 -2 2 compfreq.present compfreq.total |
2030                     while read S P T; do
2031                         if [ ${P} -gt `expr ${T} / 2` ]; then
2032                                 echo ${S}
2033                         fi
2034                     done > comp.present
2035                 cut -f 2 -d ' ' < compfreq.total > comp.total
2036                 rm INDEX-ALL compfreq compfreq.total compfreq.present
2037
2038                 # We're done making noise.
2039                 echo "done."
2040
2041                 # Sometimes the kernel isn't installed where INDEX-ALL
2042                 # thinks that it should be: In particular, it is often in
2043                 # /boot/kernel instead of /boot/GENERIC or /boot/SMP.  To
2044                 # deal with this, if "kernel|X" is listed in comp.total
2045                 # (i.e., is a component which would be upgraded if it is
2046                 # found to be present) we will add it to comp.present.
2047                 # If "kernel|<anything>" is in comp.total but "kernel|X" is
2048                 # not, we print a warning -- the user is running a kernel
2049                 # which isn't part of the release.
2050                 KCOMP=`echo ${KERNCONF} | tr 'A-Z' 'a-z'`
2051                 grep -E "^kernel\|${KCOMP}\$" comp.total >> comp.present
2052
2053                 if grep -qE "^kernel\|" comp.total &&
2054                     ! grep -qE "^kernel\|${KCOMP}\$" comp.total; then
2055                         cat <<-EOF
2056
2057 WARNING: This system is running a "${KCOMP}" kernel, which is not a
2058 kernel configuration distributed as part of FreeBSD ${RELNUM}.
2059 This kernel will not be updated: you MUST update the kernel manually
2060 before running "$0 install".
2061                         EOF
2062                 fi
2063
2064                 # Re-sort the list of installed components and generate
2065                 # the list of non-installed components.
2066                 sort -u < comp.present > comp.present.tmp
2067                 mv comp.present.tmp comp.present
2068                 comm -13 comp.present comp.total > comp.absent
2069
2070                 # Ask the user to confirm that what we have is correct.  To
2071                 # reduce user confusion, translate "X|Y" back to "X/Y" (as
2072                 # subcomponents must be listed in the configuration file).
2073                 echo
2074                 echo -n "The following components of FreeBSD "
2075                 echo "seem to be installed:"
2076                 tr '|' '/' < comp.present |
2077                     fmt -72
2078                 echo
2079                 echo -n "The following components of FreeBSD "
2080                 echo "do not seem to be installed:"
2081                 tr '|' '/' < comp.absent |
2082                     fmt -72
2083                 echo
2084                 continuep || return 1
2085                 echo
2086
2087                 # Suck the generated list of components into ${COMPONENTS}.
2088                 # Note that comp.present.tmp is used due to issues with
2089                 # pipelines and setting variables.
2090                 COMPONENTS=""
2091                 tr '|' '/' < comp.present > comp.present.tmp
2092                 while read C; do
2093                         COMPONENTS="${COMPONENTS} ${C}"
2094                 done < comp.present.tmp
2095
2096                 # Delete temporary files
2097                 rm comp.present comp.present.tmp comp.absent comp.total
2098         fi
2099 }
2100
2101 # If StrictComponents is not "yes", COMPONENTS contains an entry
2102 # corresponding to the currently running kernel, and said kernel
2103 # does not exist in the new release, add "kernel/generic" to the
2104 # list of components.
2105 upgrade_guess_new_kernel () {
2106         if [ "${STRICTCOMPONENTS}" = "no" ]; then
2107                 # Grab the unfiltered metadata file.
2108                 METAHASH=`look "$1|" tINDEX.present | cut -f 2 -d '|'`
2109                 gunzip -c < files/${METAHASH}.gz > $1.all
2110
2111                 # If "kernel/${KCOMP}" is in ${COMPONENTS} and that component
2112                 # isn't in $1.all, we need to add kernel/generic.
2113                 for C in ${COMPONENTS}; do
2114                         if [ ${C} = "kernel/${KCOMP}" ] &&
2115                             ! grep -qE "^kernel\|${KCOMP}\|" $1.all; then
2116                                 COMPONENTS="${COMPONENTS} kernel/generic"
2117                                 NKERNCONF="GENERIC"
2118                                 cat <<-EOF
2119
2120 WARNING: This system is running a "${KCOMP}" kernel, which is not a
2121 kernel configuration distributed as part of FreeBSD ${RELNUM}.
2122 As part of upgrading to FreeBSD ${RELNUM}, this kernel will be
2123 replaced with a "generic" kernel.
2124                                 EOF
2125                                 continuep || return 1
2126                         fi
2127                 done
2128
2129                 # Don't need this any more...
2130                 rm $1.all
2131         fi
2132 }
2133
2134 # Convert INDEX-OLD (last release) and INDEX-ALL (new release) into
2135 # INDEX-OLD and INDEX-NEW files (in the sense of normal upgrades).
2136 upgrade_oldall_to_oldnew () {
2137         # For each ${F}|... which appears in INDEX-ALL but does not appear
2138         # in INDEX-OLD, add ${F}|-|||||| to INDEX-OLD.
2139         cut -f 1 -d '|' < $1 |
2140             sort -u > $1.paths
2141         cut -f 1 -d '|' < $2 |
2142             sort -u |
2143             comm -13 $1.paths - |
2144             lam - -s "|-||||||" |
2145             sort - $1 > $1.tmp
2146         mv $1.tmp $1
2147
2148         # Remove lines from INDEX-OLD which also appear in INDEX-ALL
2149         comm -23 $1 $2 > $1.tmp
2150         mv $1.tmp $1
2151
2152         # Remove lines from INDEX-ALL which have a file name not appearing
2153         # anywhere in INDEX-OLD (since these must be files which haven't
2154         # changed -- if they were new, there would be an entry of type "-").
2155         cut -f 1 -d '|' < $1 |
2156             sort -u > $1.paths
2157         sort -k 1,1 -t '|' < $2 |
2158             join -t '|' - $1.paths |
2159             sort > $2.tmp
2160         rm $1.paths
2161         mv $2.tmp $2
2162
2163         # Rename INDEX-ALL to INDEX-NEW.
2164         mv $2 $3
2165 }
2166
2167 # From the list of "old" files in $1, merge changes in $2 with those in $3,
2168 # and update $3 to reflect the hashes of merged files.
2169 upgrade_merge () {
2170         # We only need to do anything if $1 is non-empty.
2171         if [ -s $1 ]; then
2172                 cut -f 1 -d '|' $1 |
2173                     sort > $1-paths
2174
2175                 # Create staging area for merging files
2176                 rm -rf merge/
2177                 while read F; do
2178                         D=`dirname ${F}`
2179                         mkdir -p merge/old/${D}
2180                         mkdir -p merge/${OLDRELNUM}/${D}
2181                         mkdir -p merge/${RELNUM}/${D}
2182                         mkdir -p merge/new/${D}
2183                 done < $1-paths
2184
2185                 # Copy in files
2186                 while read F; do
2187                         # Currently installed file
2188                         V=`look "${F}|" $2 | cut -f 7 -d '|'`
2189                         gunzip < files/${V}.gz > merge/old/${F}
2190
2191                         # Old release
2192                         if look "${F}|" $1 | fgrep -q "|f|"; then
2193                                 V=`look "${F}|" $1 | cut -f 3 -d '|'`
2194                                 gunzip < files/${V}.gz          \
2195                                     > merge/${OLDRELNUM}/${F}
2196                         fi
2197
2198                         # New release
2199                         if look "${F}|" $3 | cut -f 1,2,7 -d '|' |
2200                             fgrep -q "|f|"; then
2201                                 V=`look "${F}|" $3 | cut -f 7 -d '|'`
2202                                 gunzip < files/${V}.gz          \
2203                                     > merge/${RELNUM}/${F}
2204                         fi
2205                 done < $1-paths
2206
2207                 # Attempt to automatically merge changes
2208                 echo -n "Attempting to automatically merge "
2209                 echo -n "changes in files..."
2210                 : > failed.merges
2211                 while read F; do
2212                         # If the file doesn't exist in the new release,
2213                         # the result of "merging changes" is having the file
2214                         # not exist.
2215                         if ! [ -f merge/${RELNUM}/${F} ]; then
2216                                 continue
2217                         fi
2218
2219                         # If the file didn't exist in the old release, we're
2220                         # going to throw away the existing file and hope that
2221                         # the version from the new release is what we want.
2222                         if ! [ -f merge/${OLDRELNUM}/${F} ]; then
2223                                 cp merge/${RELNUM}/${F} merge/new/${F}
2224                                 continue
2225                         fi
2226
2227                         # Some files need special treatment.
2228                         case ${F} in
2229                         /etc/spwd.db | /etc/pwd.db | /etc/login.conf.db)
2230                                 # Don't merge these -- we're rebuild them
2231                                 # after updates are installed.
2232                                 cp merge/old/${F} merge/new/${F}
2233                                 ;;
2234                         *)
2235                                 if ! merge -p -L "current version"      \
2236                                     -L "${OLDRELNUM}" -L "${RELNUM}"    \
2237                                     merge/old/${F}                      \
2238                                     merge/${OLDRELNUM}/${F}             \
2239                                     merge/${RELNUM}/${F}                \
2240                                     > merge/new/${F} 2>/dev/null; then
2241                                         echo ${F} >> failed.merges
2242                                 fi
2243                                 ;;
2244                         esac
2245                 done < $1-paths
2246                 echo " done."
2247
2248                 # Ask the user to handle any files which didn't merge.
2249                 while read F; do
2250                         cat <<-EOF
2251
2252 The following file could not be merged automatically: ${F}
2253 Press Enter to edit this file in ${EDITOR} and resolve the conflicts
2254 manually...
2255                         EOF
2256                         read dummy </dev/tty
2257                         ${EDITOR} `pwd`/merge/new/${F} < /dev/tty
2258                 done < failed.merges
2259                 rm failed.merges
2260
2261                 # Ask the user to confirm that he likes how the result
2262                 # of merging files.
2263                 while read F; do
2264                         # Skip files which haven't changed.
2265                         if [ -f merge/new/${F} ] &&
2266                             cmp -s merge/old/${F} merge/new/${F}; then
2267                                 continue
2268                         fi
2269
2270                         # Warn about files which are ceasing to exist.
2271                         if ! [ -f merge/new/${F} ]; then
2272                                 cat <<-EOF
2273
2274 The following file will be removed, as it no longer exists in
2275 FreeBSD ${RELNUM}: ${F}
2276                                 EOF
2277                                 continuep < /dev/tty || return 1
2278                                 continue
2279                         fi
2280
2281                         # Print changes for the user's approval.
2282                         cat <<-EOF
2283
2284 The following changes, which occurred between FreeBSD ${OLDRELNUM} and
2285 FreeBSD ${RELNUM} have been merged into ${F}:
2286 EOF
2287                         diff -U 5 -L "current version" -L "new version" \
2288                             merge/old/${F} merge/new/${F} || true
2289                         continuep < /dev/tty || return 1
2290                 done < $1-paths
2291
2292                 # Store merged files.
2293                 while read F; do
2294                         if [ -f merge/new/${F} ]; then
2295                                 V=`${SHA256} -q merge/new/${F}`
2296
2297                                 gzip -c < merge/new/${F} > files/${V}.gz
2298                                 echo "${F}|${V}"
2299                         fi
2300                 done < $1-paths > newhashes
2301
2302                 # Pull lines out from $3 which need to be updated to
2303                 # reflect merged files.
2304                 while read F; do
2305                         look "${F}|" $3
2306                 done < $1-paths > $3-oldlines
2307
2308                 # Update lines to reflect merged files
2309                 join -t '|' -o 1.1,1.2,1.3,1.4,1.5,1.6,2.2,1.8          \
2310                     $3-oldlines newhashes > $3-newlines
2311
2312                 # Remove old lines from $3 and add new lines.
2313                 sort $3-oldlines |
2314                     comm -13 - $3 |
2315                     sort - $3-newlines > $3.tmp
2316                 mv $3.tmp $3
2317
2318                 # Clean up
2319                 rm $1-paths newhashes $3-oldlines $3-newlines
2320                 rm -rf merge/
2321         fi
2322
2323         # We're done with merging files.
2324         rm $1
2325 }
2326
2327 # Do the work involved in fetching upgrades to a new release
2328 upgrade_run () {
2329         workdir_init || return 1
2330
2331         # Prepare the mirror list.
2332         fetch_pick_server_init && fetch_pick_server
2333
2334         # Try to fetch the public key until we run out of servers.
2335         while ! fetch_key; do
2336                 fetch_pick_server || return 1
2337         done
2338  
2339         # Try to fetch the metadata index signature ("tag") until we run
2340         # out of available servers; and sanity check the downloaded tag.
2341         while ! fetch_tag; do
2342                 fetch_pick_server || return 1
2343         done
2344         fetch_tagsanity || return 1
2345
2346         # Fetch the INDEX-OLD and INDEX-ALL.
2347         fetch_metadata INDEX-OLD INDEX-ALL || return 1
2348
2349         # If StrictComponents is not "yes", generate a new components list
2350         # with only the components which appear to be installed.
2351         upgrade_guess_components INDEX-ALL || return 1
2352
2353         # Generate filtered INDEX-OLD and INDEX-ALL files containing only
2354         # the components we want and without anything marked as "Ignore".
2355         fetch_filter_metadata INDEX-OLD || return 1
2356         fetch_filter_metadata INDEX-ALL || return 1
2357
2358         # Merge the INDEX-OLD and INDEX-ALL files into INDEX-OLD.
2359         sort INDEX-OLD INDEX-ALL > INDEX-OLD.tmp
2360         mv INDEX-OLD.tmp INDEX-OLD
2361         rm INDEX-ALL
2362
2363         # Adjust variables for fetching files from the new release.
2364         OLDRELNUM=${RELNUM}
2365         RELNUM=${TARGETRELEASE}
2366         OLDFETCHDIR=${FETCHDIR}
2367         FETCHDIR=${RELNUM}/${ARCH}
2368
2369         # Try to fetch the NEW metadata index signature ("tag") until we run
2370         # out of available servers; and sanity check the downloaded tag.
2371         while ! fetch_tag; do
2372                 fetch_pick_server || return 1
2373         done
2374
2375         # Fetch the new INDEX-ALL.
2376         fetch_metadata INDEX-ALL || return 1
2377
2378         # If StrictComponents is not "yes", COMPONENTS contains an entry
2379         # corresponding to the currently running kernel, and said kernel
2380         # does not exist in the new release, add "kernel/generic" to the
2381         # list of components.
2382         upgrade_guess_new_kernel INDEX-ALL || return 1
2383
2384         # Filter INDEX-ALL to contain only the components we want and without
2385         # anything marked as "Ignore".
2386         fetch_filter_metadata INDEX-ALL || return 1
2387
2388         # Convert INDEX-OLD (last release) and INDEX-ALL (new release) into
2389         # INDEX-OLD and INDEX-NEW files (in the sense of normal upgrades).
2390         upgrade_oldall_to_oldnew INDEX-OLD INDEX-ALL INDEX-NEW
2391
2392         # Translate /boot/${KERNCONF} or /boot/${NKERNCONF} into ${KERNELDIR}
2393         fetch_filter_kernel_names INDEX-NEW ${NKERNCONF}
2394         fetch_filter_kernel_names INDEX-OLD ${KERNCONF}
2395
2396         # For all paths appearing in INDEX-OLD or INDEX-NEW, inspect the
2397         # system and generate an INDEX-PRESENT file.
2398         fetch_inspect_system INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1
2399
2400         # Based on ${MERGECHANGES}, generate a file tomerge-old with the
2401         # paths and hashes of old versions of files to merge.
2402         fetch_filter_mergechanges INDEX-OLD INDEX-PRESENT tomerge-old
2403
2404         # Based on ${UPDATEIFUNMODIFIED}, remove lines from INDEX-* which
2405         # correspond to lines in INDEX-PRESENT with hashes not appearing
2406         # in INDEX-OLD or INDEX-NEW.  Also remove lines where the entry in
2407         # INDEX-PRESENT has type - and there isn't a corresponding entry in
2408         # INDEX-OLD with type -.
2409         fetch_filter_unmodified_notpresent      \
2410             INDEX-OLD INDEX-PRESENT INDEX-NEW tomerge-old
2411
2412         # For each entry in INDEX-PRESENT of type -, remove any corresponding
2413         # entry from INDEX-NEW if ${ALLOWADD} != "yes".  Remove all entries
2414         # of type - from INDEX-PRESENT.
2415         fetch_filter_allowadd INDEX-PRESENT INDEX-NEW
2416
2417         # If ${ALLOWDELETE} != "yes", then remove any entries from
2418         # INDEX-PRESENT which don't correspond to entries in INDEX-NEW.
2419         fetch_filter_allowdelete INDEX-PRESENT INDEX-NEW
2420
2421         # If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in
2422         # INDEX-PRESENT with metadata not matching any entry in INDEX-OLD,
2423         # replace the corresponding line of INDEX-NEW with one having the
2424         # same metadata as the entry in INDEX-PRESENT.
2425         fetch_filter_modified_metadata INDEX-OLD INDEX-PRESENT INDEX-NEW
2426
2427         # Remove lines from INDEX-PRESENT and INDEX-NEW which are identical;
2428         # no need to update a file if it isn't changing.
2429         fetch_filter_uptodate INDEX-PRESENT INDEX-NEW
2430
2431         # Fetch "clean" files from the old release for merging changes.
2432         fetch_files_premerge tomerge-old
2433
2434         # Prepare to fetch files: Generate a list of the files we need,
2435         # copy the unmodified files we have into /files/, and generate
2436         # a list of patches to download.
2437         fetch_files_prepare INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1
2438
2439         # Fetch patches from to-${RELNUM}/${ARCH}/bp/
2440         PATCHDIR=to-${RELNUM}/${ARCH}/bp
2441         fetch_files || return 1
2442
2443         # Merge configuration file changes.
2444         upgrade_merge tomerge-old INDEX-PRESENT INDEX-NEW || return 1
2445
2446         # Create and populate install manifest directory; and report what
2447         # updates are available.
2448         fetch_create_manifest || return 1
2449
2450         # Leave a note behind to tell the "install" command that the kernel
2451         # needs to be installed before the world.
2452         touch ${BDHASH}-install/kernelfirst
2453 }
2454
2455 # Make sure that all the file hashes mentioned in $@ have corresponding
2456 # gzipped files stored in /files/.
2457 install_verify () {
2458         # Generate a list of hashes
2459         cat $@ |
2460             cut -f 2,7 -d '|' |
2461             grep -E '^f' |
2462             cut -f 2 -d '|' |
2463             sort -u > filelist
2464
2465         # Make sure all the hashes exist
2466         while read HASH; do
2467                 if ! [ -f files/${HASH}.gz ]; then
2468                         echo -n "Update files missing -- "
2469                         echo "this should never happen."
2470                         echo "Re-run '$0 fetch'."
2471                         return 1
2472                 fi
2473         done < filelist
2474
2475         # Clean up
2476         rm filelist
2477 }
2478
2479 # Remove the system immutable flag from files
2480 install_unschg () {
2481         # Generate file list
2482         cat $@ |
2483             cut -f 1 -d '|' > filelist
2484
2485         # Remove flags
2486         while read F; do
2487                 if ! [ -e ${BASEDIR}/${F} ]; then
2488                         continue
2489                 fi
2490
2491                 chflags noschg ${BASEDIR}/${F} || return 1
2492         done < filelist
2493
2494         # Clean up
2495         rm filelist
2496 }
2497
2498 # Install new files
2499 install_from_index () {
2500         # First pass: Do everything apart from setting file flags.  We
2501         # can't set flags yet, because schg inhibits hard linking.
2502         sort -k 1,1 -t '|' $1 |
2503             tr '|' ' ' |
2504             while read FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK; do
2505                 case ${TYPE} in
2506                 d)
2507                         # Create a directory
2508                         install -d -o ${OWNER} -g ${GROUP}              \
2509                             -m ${PERM} ${BASEDIR}/${FPATH}
2510                         ;;
2511                 f)
2512                         if [ -z "${LINK}" ]; then
2513                                 # Create a file, without setting flags.
2514                                 gunzip < files/${HASH}.gz > ${HASH}
2515                                 install -S -o ${OWNER} -g ${GROUP}      \
2516                                     -m ${PERM} ${HASH} ${BASEDIR}/${FPATH}
2517                                 rm ${HASH}
2518                         else
2519                                 # Create a hard link.
2520                                 ln -f ${BASEDIR}/${LINK} ${BASEDIR}/${FPATH}
2521                         fi
2522                         ;;
2523                 L)
2524                         # Create a symlink
2525                         ln -sfh ${HASH} ${BASEDIR}/${FPATH}
2526                         ;;
2527                 esac
2528             done
2529
2530         # Perform a second pass, adding file flags.
2531         tr '|' ' ' < $1 |
2532             while read FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK; do
2533                 if [ ${TYPE} = "f" ] &&
2534                     ! [ ${FLAGS} = "0" ]; then
2535                         chflags ${FLAGS} ${BASEDIR}/${FPATH}
2536                 fi
2537             done
2538 }
2539
2540 # Remove files which we want to delete
2541 install_delete () {
2542         # Generate list of new files
2543         cut -f 1 -d '|' < $2 |
2544             sort > newfiles
2545
2546         # Generate subindex of old files we want to nuke
2547         sort -k 1,1 -t '|' $1 |
2548             join -t '|' -v 1 - newfiles |
2549             sort -r -k 1,1 -t '|' |
2550             cut -f 1,2 -d '|' |
2551             tr '|' ' ' > killfiles
2552
2553         # Remove the offending bits
2554         while read FPATH TYPE; do
2555                 case ${TYPE} in
2556                 d)
2557                         rmdir ${BASEDIR}/${FPATH}
2558                         ;;
2559                 f)
2560                         rm ${BASEDIR}/${FPATH}
2561                         ;;
2562                 L)
2563                         rm ${BASEDIR}/${FPATH}
2564                         ;;
2565                 esac
2566         done < killfiles
2567
2568         # Clean up
2569         rm newfiles killfiles
2570 }
2571
2572 # Install new files, delete old files, and update linker.hints
2573 install_files () {
2574         # If we haven't already dealt with the kernel, deal with it.
2575         if ! [ -f $1/kerneldone ]; then
2576                 grep -E '^/boot/' $1/INDEX-OLD > INDEX-OLD
2577                 grep -E '^/boot/' $1/INDEX-NEW > INDEX-NEW
2578
2579                 # Install new files
2580                 install_from_index INDEX-NEW || return 1
2581
2582                 # Remove files which need to be deleted
2583                 install_delete INDEX-OLD INDEX-NEW || return 1
2584
2585                 # Update linker.hints if necessary
2586                 if [ -s INDEX-OLD -o -s INDEX-NEW ]; then
2587                         kldxref -R /boot/ 2>/dev/null
2588                 fi
2589
2590                 # We've finished updating the kernel.
2591                 touch $1/kerneldone
2592
2593                 # Do we need to ask for a reboot now?
2594                 if [ -f $1/kernelfirst ] &&
2595                     [ -s INDEX-OLD -o -s INDEX-NEW ]; then
2596                         cat <<-EOF
2597
2598 Kernel updates have been installed.  Please reboot and run
2599 "$0 install" again to finish installing updates.
2600                         EOF
2601                         exit 0
2602                 fi
2603         fi
2604
2605         # If we haven't already dealt with the world, deal with it.
2606         if ! [ -f $1/worlddone ]; then
2607                 # Install new shared libraries next
2608                 grep -vE '^/boot/' $1/INDEX-NEW |
2609                     grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2610                 install_from_index INDEX-NEW || return 1
2611
2612                 # Deal with everything else
2613                 grep -vE '^/boot/' $1/INDEX-OLD |
2614                     grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD
2615                 grep -vE '^/boot/' $1/INDEX-NEW |
2616                     grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2617                 install_from_index INDEX-NEW || return 1
2618                 install_delete INDEX-OLD INDEX-NEW || return 1
2619
2620                 # Rebuild /etc/spwd.db and /etc/pwd.db if necessary.
2621                 if [ /etc/master.passwd -nt /etc/spwd.db ] ||
2622                     [ /etc/master.passwd -nt /etc/pwd.db ]; then
2623                         pwd_mkdb /etc/master.passwd
2624                 fi
2625
2626                 # Rebuild /etc/login.conf.db if necessary.
2627                 if [ /etc/login.conf -nt /etc/login.conf.db ]; then
2628                         cap_mkdb /etc/login.conf
2629                 fi
2630
2631                 # We've finished installing the world and deleting old files
2632                 # which are not shared libraries.
2633                 touch $1/worlddone
2634
2635                 # Do we need to ask the user to portupgrade now?
2636                 grep -vE '^/boot/' $1/INDEX-NEW |
2637                     grep -E '/lib/.*\.so\.[0-9]+\|' |
2638                     cut -f 1 -d '|' |
2639                     sort > newfiles
2640                 if grep -vE '^/boot/' $1/INDEX-OLD |
2641                     grep -E '/lib/.*\.so\.[0-9]+\|' |
2642                     cut -f 1 -d '|' |
2643                     sort |
2644                     join -v 1 - newfiles |
2645                     grep -q .; then
2646                         cat <<-EOF
2647
2648 Completing this upgrade requires removing old shared object files.
2649 Please rebuild all installed 3rd party software (e.g., programs
2650 installed from the ports tree) and then run "$0 install"
2651 again to finish installing updates.
2652                         EOF
2653                         rm newfiles
2654                         exit 0
2655                 fi
2656                 rm newfiles
2657         fi
2658
2659         # Remove old shared libraries
2660         grep -vE '^/boot/' $1/INDEX-NEW |
2661             grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2662         grep -vE '^/boot/' $1/INDEX-OLD |
2663             grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD
2664         install_delete INDEX-OLD INDEX-NEW || return 1
2665
2666         # Remove temporary files
2667         rm INDEX-OLD INDEX-NEW
2668 }
2669
2670 # Rearrange bits to allow the installed updates to be rolled back
2671 install_setup_rollback () {
2672         # Remove the "reboot after installing kernel", "kernel updated", and
2673         # "finished installing the world" flags if present -- they are
2674         # irrelevant when rolling back updates.
2675         if [ -f ${BDHASH}-install/kernelfirst ]; then
2676                 rm ${BDHASH}-install/kernelfirst
2677                 rm ${BDHASH}-install/kerneldone
2678         fi
2679         if [ -f ${BDHASH}-install/worlddone ]; then
2680                 rm ${BDHASH}-install/worlddone
2681         fi
2682
2683         if [ -L ${BDHASH}-rollback ]; then
2684                 mv ${BDHASH}-rollback ${BDHASH}-install/rollback
2685         fi
2686
2687         mv ${BDHASH}-install ${BDHASH}-rollback
2688 }
2689
2690 # Actually install updates
2691 install_run () {
2692         echo -n "Installing updates..."
2693
2694         # Make sure we have all the files we should have
2695         install_verify ${BDHASH}-install/INDEX-OLD      \
2696             ${BDHASH}-install/INDEX-NEW || return 1
2697
2698         # Remove system immutable flag from files
2699         install_unschg ${BDHASH}-install/INDEX-OLD      \
2700             ${BDHASH}-install/INDEX-NEW || return 1
2701
2702         # Install new files, delete old files, and update linker.hints
2703         install_files ${BDHASH}-install || return 1
2704
2705         # Rearrange bits to allow the installed updates to be rolled back
2706         install_setup_rollback
2707
2708         echo " done."
2709 }
2710
2711 # Rearrange bits to allow the previous set of updates to be rolled back next.
2712 rollback_setup_rollback () {
2713         if [ -L ${BDHASH}-rollback/rollback ]; then
2714                 mv ${BDHASH}-rollback/rollback rollback-tmp
2715                 rm -r ${BDHASH}-rollback/
2716                 rm ${BDHASH}-rollback
2717                 mv rollback-tmp ${BDHASH}-rollback
2718         else
2719                 rm -r ${BDHASH}-rollback/
2720                 rm ${BDHASH}-rollback
2721         fi
2722 }
2723
2724 # Install old files, delete new files, and update linker.hints
2725 rollback_files () {
2726         # Install old shared library files which don't have the same path as
2727         # a new shared library file.
2728         grep -vE '^/boot/' $1/INDEX-NEW |
2729             grep -E '/lib/.*\.so\.[0-9]+\|' |
2730             cut -f 1 -d '|' |
2731             sort > INDEX-NEW.libs.flist
2732         grep -vE '^/boot/' $1/INDEX-OLD |
2733             grep -E '/lib/.*\.so\.[0-9]+\|' |
2734             sort -k 1,1 -t '|' - |
2735             join -t '|' -v 1 - INDEX-NEW.libs.flist > INDEX-OLD
2736         install_from_index INDEX-OLD || return 1
2737
2738         # Deal with files which are neither kernel nor shared library
2739         grep -vE '^/boot/' $1/INDEX-OLD |
2740             grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD
2741         grep -vE '^/boot/' $1/INDEX-NEW |
2742             grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2743         install_from_index INDEX-OLD || return 1
2744         install_delete INDEX-NEW INDEX-OLD || return 1
2745
2746         # Install any old shared library files which we didn't install above.
2747         grep -vE '^/boot/' $1/INDEX-OLD |
2748             grep -E '/lib/.*\.so\.[0-9]+\|' |
2749             sort -k 1,1 -t '|' - |
2750             join -t '|' - INDEX-NEW.libs.flist > INDEX-OLD
2751         install_from_index INDEX-OLD || return 1
2752
2753         # Delete unneeded shared library files
2754         grep -vE '^/boot/' $1/INDEX-OLD |
2755             grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD
2756         grep -vE '^/boot/' $1/INDEX-NEW |
2757             grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2758         install_delete INDEX-NEW INDEX-OLD || return 1
2759
2760         # Deal with kernel files
2761         grep -E '^/boot/' $1/INDEX-OLD > INDEX-OLD
2762         grep -E '^/boot/' $1/INDEX-NEW > INDEX-NEW
2763         install_from_index INDEX-OLD || return 1
2764         install_delete INDEX-NEW INDEX-OLD || return 1
2765         if [ -s INDEX-OLD -o -s INDEX-NEW ]; then
2766                 kldxref -R /boot/ 2>/dev/null
2767         fi
2768
2769         # Remove temporary files
2770         rm INDEX-OLD INDEX-NEW INDEX-NEW.libs.flist
2771 }
2772
2773 # Actually rollback updates
2774 rollback_run () {
2775         echo -n "Uninstalling updates..."
2776
2777         # If there are updates waiting to be installed, remove them; we
2778         # want the user to re-run 'fetch' after rolling back updates.
2779         if [ -L ${BDHASH}-install ]; then
2780                 rm -r ${BDHASH}-install/
2781                 rm ${BDHASH}-install
2782         fi
2783
2784         # Make sure we have all the files we should have
2785         install_verify ${BDHASH}-rollback/INDEX-NEW     \
2786             ${BDHASH}-rollback/INDEX-OLD || return 1
2787
2788         # Remove system immutable flag from files
2789         install_unschg ${BDHASH}-rollback/INDEX-NEW     \
2790             ${BDHASH}-rollback/INDEX-OLD || return 1
2791
2792         # Install old files, delete new files, and update linker.hints
2793         rollback_files ${BDHASH}-rollback || return 1
2794
2795         # Remove the rollback directory and the symlink pointing to it; and
2796         # rearrange bits to allow the previous set of updates to be rolled
2797         # back next.
2798         rollback_setup_rollback
2799
2800         echo " done."
2801 }
2802
2803 # Compare INDEX-ALL and INDEX-PRESENT and print warnings about differences.
2804 IDS_compare () {
2805         # Get all the lines which mismatch in something other than file
2806         # flags.  We ignore file flags because sysinstall doesn't seem to
2807         # set them when it installs FreeBSD; warning about these adds a
2808         # very large amount of noise.
2809         cut -f 1-5,7-8 -d '|' $1 > $1.noflags
2810         sort -k 1,1 -t '|' $1.noflags > $1.sorted
2811         cut -f 1-5,7-8 -d '|' $2 |
2812             comm -13 $1.noflags - |
2813             fgrep -v '|-|||||' |
2814             sort -k 1,1 -t '|' |
2815             join -t '|' $1.sorted - > INDEX-NOTMATCHING
2816
2817         # Ignore files which match IDSIGNOREPATHS.
2818         for X in ${IDSIGNOREPATHS}; do
2819                 grep -E "^${X}" INDEX-NOTMATCHING
2820         done |
2821             sort -u |
2822             comm -13 - INDEX-NOTMATCHING > INDEX-NOTMATCHING.tmp
2823         mv INDEX-NOTMATCHING.tmp INDEX-NOTMATCHING
2824
2825         # Go through the lines and print warnings.
2826         while read LINE; do
2827                 FPATH=`echo "${LINE}" | cut -f 1 -d '|'`
2828                 TYPE=`echo "${LINE}" | cut -f 2 -d '|'`
2829                 OWNER=`echo "${LINE}" | cut -f 3 -d '|'`
2830                 GROUP=`echo "${LINE}" | cut -f 4 -d '|'`
2831                 PERM=`echo "${LINE}" | cut -f 5 -d '|'`
2832                 HASH=`echo "${LINE}" | cut -f 6 -d '|'`
2833                 LINK=`echo "${LINE}" | cut -f 7 -d '|'`
2834                 P_TYPE=`echo "${LINE}" | cut -f 8 -d '|'`
2835                 P_OWNER=`echo "${LINE}" | cut -f 9 -d '|'`
2836                 P_GROUP=`echo "${LINE}" | cut -f 10 -d '|'`
2837                 P_PERM=`echo "${LINE}" | cut -f 11 -d '|'`
2838                 P_HASH=`echo "${LINE}" | cut -f 12 -d '|'`
2839                 P_LINK=`echo "${LINE}" | cut -f 13 -d '|'`
2840
2841                 # Warn about different object types.
2842                 if ! [ "${TYPE}" = "${P_TYPE}" ]; then
2843                         echo -n "${FPATH} is a "
2844                         case "${P_TYPE}" in
2845                         f)      echo -n "regular file, "
2846                                 ;;
2847                         d)      echo -n "directory, "
2848                                 ;;
2849                         L)      echo -n "symlink, "
2850                                 ;;
2851                         esac
2852                         echo -n "but should be a "
2853                         case "${TYPE}" in
2854                         f)      echo -n "regular file."
2855                                 ;;
2856                         d)      echo -n "directory."
2857                                 ;;
2858                         L)      echo -n "symlink."
2859                                 ;;
2860                         esac
2861                         echo
2862
2863                         # Skip other tests, since they don't make sense if
2864                         # we're comparing different object types.
2865                         continue
2866                 fi
2867
2868                 # Warn about different owners.
2869                 if ! [ "${OWNER}" = "${P_OWNER}" ]; then
2870                         echo -n "${FPATH} is owned by user id ${P_OWNER}, "
2871                         echo "but should be owned by user id ${OWNER}."
2872                 fi
2873
2874                 # Warn about different groups.
2875                 if ! [ "${GROUP}" = "${P_GROUP}" ]; then
2876                         echo -n "${FPATH} is owned by group id ${P_GROUP}, "
2877                         echo "but should be owned by group id ${GROUP}."
2878                 fi
2879
2880                 # Warn about different permissions.  We do not warn about
2881                 # different permissions on symlinks, since some archivers
2882                 # don't extract symlink permissions correctly and they are
2883                 # ignored anyway.
2884                 if ! [ "${PERM}" = "${P_PERM}" ] &&
2885                     ! [ "${TYPE}" = "L" ]; then
2886                         echo -n "${FPATH} has ${P_PERM} permissions, "
2887                         echo "but should have ${PERM} permissions."
2888                 fi
2889
2890                 # Warn about different file hashes / symlink destinations.
2891                 if ! [ "${HASH}" = "${P_HASH}" ]; then
2892                         if [ "${TYPE}" = "L" ]; then
2893                                 echo -n "${FPATH} is a symlink to ${P_HASH}, "
2894                                 echo "but should be a symlink to ${HASH}."
2895                         fi
2896                         if [ "${TYPE}" = "f" ]; then
2897                                 echo -n "${FPATH} has SHA256 hash ${P_HASH}, "
2898                                 echo "but should have SHA256 hash ${HASH}."
2899                         fi
2900                 fi
2901
2902                 # We don't warn about different hard links, since some
2903                 # some archivers break hard links, and as long as the
2904                 # underlying data is correct they really don't matter.
2905         done < INDEX-NOTMATCHING
2906
2907         # Clean up
2908         rm $1 $1.noflags $1.sorted $2 INDEX-NOTMATCHING
2909 }
2910
2911 # Do the work involved in comparing the system to a "known good" index
2912 IDS_run () {
2913         workdir_init || return 1
2914
2915         # Prepare the mirror list.
2916         fetch_pick_server_init && fetch_pick_server
2917
2918         # Try to fetch the public key until we run out of servers.
2919         while ! fetch_key; do
2920                 fetch_pick_server || return 1
2921         done
2922  
2923         # Try to fetch the metadata index signature ("tag") until we run
2924         # out of available servers; and sanity check the downloaded tag.
2925         while ! fetch_tag; do
2926                 fetch_pick_server || return 1
2927         done
2928         fetch_tagsanity || return 1
2929
2930         # Fetch INDEX-OLD and INDEX-ALL.
2931         fetch_metadata INDEX-OLD INDEX-ALL || return 1
2932
2933         # Generate filtered INDEX-OLD and INDEX-ALL files containing only
2934         # the components we want and without anything marked as "Ignore".
2935         fetch_filter_metadata INDEX-OLD || return 1
2936         fetch_filter_metadata INDEX-ALL || return 1
2937
2938         # Merge the INDEX-OLD and INDEX-ALL files into INDEX-ALL.
2939         sort INDEX-OLD INDEX-ALL > INDEX-ALL.tmp
2940         mv INDEX-ALL.tmp INDEX-ALL
2941         rm INDEX-OLD
2942
2943         # Translate /boot/${KERNCONF} to ${KERNELDIR}
2944         fetch_filter_kernel_names INDEX-ALL ${KERNCONF}
2945
2946         # Inspect the system and generate an INDEX-PRESENT file.
2947         fetch_inspect_system INDEX-ALL INDEX-PRESENT /dev/null || return 1
2948
2949         # Compare INDEX-ALL and INDEX-PRESENT and print warnings about any
2950         # differences.
2951         IDS_compare INDEX-ALL INDEX-PRESENT
2952 }
2953
2954 #### Main functions -- call parameter-handling and core functions
2955
2956 # Using the command line, configuration file, and defaults,
2957 # set all the parameters which are needed later.
2958 get_params () {
2959         init_params
2960         parse_cmdline $@
2961         parse_conffile
2962         default_params
2963 }
2964
2965 # Fetch command.  Make sure that we're being called
2966 # interactively, then run fetch_check_params and fetch_run
2967 cmd_fetch () {
2968         if [ ! -t 0 ]; then
2969                 echo -n "`basename $0` fetch should not "
2970                 echo "be run non-interactively."
2971                 echo "Run `basename $0` cron instead."
2972                 exit 1
2973         fi
2974         fetch_check_params
2975         fetch_run || exit 1
2976 }
2977
2978 # Cron command.  Make sure the parameters are sensible; wait
2979 # rand(3600) seconds; then fetch updates.  While fetching updates,
2980 # send output to a temporary file; only print that file if the
2981 # fetching failed.
2982 cmd_cron () {
2983         fetch_check_params
2984         sleep `jot -r 1 0 3600`
2985
2986         TMPFILE=`mktemp /tmp/freebsd-update.XXXXXX` || exit 1
2987         if ! fetch_run >> ${TMPFILE} ||
2988             ! grep -q "No updates needed" ${TMPFILE} ||
2989             [ ${VERBOSELEVEL} = "debug" ]; then
2990                 mail -s "`hostname` security updates" ${MAILTO} < ${TMPFILE}
2991         fi
2992
2993         rm ${TMPFILE}
2994 }
2995
2996 # Fetch files for upgrading to a new release.
2997 cmd_upgrade () {
2998         upgrade_check_params
2999         upgrade_run || exit 1
3000 }
3001
3002 # Install downloaded updates.
3003 cmd_install () {
3004         install_check_params
3005         install_run || exit 1
3006 }
3007
3008 # Rollback most recently installed updates.
3009 cmd_rollback () {
3010         rollback_check_params
3011         rollback_run || exit 1
3012 }
3013
3014 # Compare system against a "known good" index.
3015 cmd_IDS () {
3016         IDS_check_params
3017         IDS_run || exit 1
3018 }
3019
3020 #### Entry point
3021
3022 # Make sure we find utilities from the base system
3023 export PATH=/sbin:/bin:/usr/sbin:/usr/bin:${PATH}
3024
3025 # Set LC_ALL in order to avoid problems with character ranges like [A-Z].
3026 export LC_ALL=C
3027
3028 get_params $@
3029 for COMMAND in ${COMMANDS}; do
3030         cmd_${COMMAND}
3031 done