github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/cmd/snap-bootstrap/initramfs_systemd_mount.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"time"
    29  
    30  	"github.com/snapcore/snapd/dirs"
    31  	"github.com/snapcore/snapd/osutil"
    32  	"github.com/snapcore/snapd/systemd"
    33  )
    34  
    35  var (
    36  	timeNow = time.Now
    37  
    38  	// default 1:30, as that is how long systemd will wait for services by
    39  	// default so seems a sensible default
    40  	defaultMountUnitWaitTimeout = time.Minute + 30*time.Second
    41  
    42  	unitFileDependOverride = `[Unit]
    43  Requires=%[1]s
    44  After=%[1]s
    45  `
    46  
    47  	doSystemdMount = doSystemdMountImpl
    48  )
    49  
    50  // systemdMountOptions reflects the set of options for mounting something using
    51  // systemd-mount(1)
    52  type systemdMountOptions struct {
    53  	// Tmpfs indicates that "what" should be ignored and a new tmpfs should be
    54  	// mounted at the location.
    55  	Tmpfs bool
    56  	// Ephemeral indicates that the mount should not persist from the initramfs
    57  	// to after the pivot_root to normal userspace. The default value, false,
    58  	// means that the mount will persist across the transition, this is done by
    59  	// creating systemd unit overrides for various initrd targets in /run that
    60  	// systemd understands when it isolates to the initrd-cleanup.target when
    61  	// the pivot_root is performed.
    62  	Ephemeral bool
    63  	// NeedsFsck indicates that before returning to the caller, an fsck check
    64  	// should be performed on the thing being mounted.
    65  	NeedsFsck bool
    66  	// NoWait will not wait until the systemd unit is active and running, which
    67  	// is the default behavior.
    68  	NoWait bool
    69  	// NoSuid indicates that the partition should be mounted with nosuid set on
    70  	// it to prevent suid execution.
    71  	NoSuid bool
    72  }
    73  
    74  // doSystemdMount will mount "what" at "where" using systemd-mount(1) with
    75  // various options. Note that in some error cases, the mount unit may have
    76  // already been created and it will not be deleted here, if that is the case
    77  // callers should check manually if the unit needs to be removed on error
    78  // conditions.
    79  func doSystemdMountImpl(what, where string, opts *systemdMountOptions) error {
    80  	if opts == nil {
    81  		opts = &systemdMountOptions{}
    82  	}
    83  
    84  	// doesn't make sense to fsck a tmpfs
    85  	if opts.NeedsFsck && opts.Tmpfs {
    86  		return fmt.Errorf("cannot mount %q at %q: impossible to fsck a tmpfs", what, where)
    87  	}
    88  
    89  	whereEscaped := systemd.EscapeUnitNamePath(where)
    90  	unitName := whereEscaped + ".mount"
    91  
    92  	args := []string{what, where, "--no-pager", "--no-ask-password"}
    93  	if opts.Tmpfs {
    94  		args = append(args, "--type=tmpfs")
    95  	}
    96  
    97  	if opts.NeedsFsck {
    98  		// note that with the --fsck=yes argument, systemd will block starting
    99  		// the mount unit on a new systemd-fsck@<what> unit that will run the
   100  		// fsck, so we don't need to worry about waiting for that to finish in
   101  		// the case where we are supposed to wait (which is the default for this
   102  		// function)
   103  		args = append(args, "--fsck=yes")
   104  	} else {
   105  		// the default is to use fsck=yes, so if it doesn't need fsck we need to
   106  		// explicitly turn it off
   107  		args = append(args, "--fsck=no")
   108  	}
   109  
   110  	// Under all circumstances that we use systemd-mount here from
   111  	// snap-bootstrap, it is expected to be okay to block waiting for the unit
   112  	// to be started and become active, because snap-bootstrap is, by design,
   113  	// expected to run as late as possible in the initramfs, and so any
   114  	// dependencies there might be in systemd creating and starting these mount
   115  	// units should already be ready and so we will not block forever. If
   116  	// however there was something going on in systemd at the same time that the
   117  	// mount unit depended on, we could hit a deadlock blocking as systemd will
   118  	// not enqueue this job until it's dependencies are ready, and so if those
   119  	// things depend on this mount unit we are stuck. The solution to this
   120  	// situation is to make snap-bootstrap run as late as possible before
   121  	// mounting things.
   122  	// However, we leave in the option to not block if there is ever a reason
   123  	// we need to do so.
   124  	if opts.NoWait {
   125  		args = append(args, "--no-block")
   126  	}
   127  
   128  	if opts.NoSuid {
   129  		args = append(args, "--options=nosuid")
   130  	}
   131  
   132  	// note that we do not currently parse any output from systemd-mount, but if
   133  	// we ever do, take special care surrounding the debug output that systemd
   134  	// outputs with the "debug" kernel command line present (or equivalently the
   135  	// SYSTEMD_LOG_LEVEL=debug env var) which will add lots of additional output
   136  	// to stderr from systemd commands
   137  	out, err := exec.Command("systemd-mount", args...).CombinedOutput()
   138  	if err != nil {
   139  		return osutil.OutputErr(out, err)
   140  	}
   141  
   142  	// if it should survive pivot_root() then we need to add overrides for this
   143  	// unit to /run/systemd units
   144  	if !opts.Ephemeral {
   145  		// to survive the pivot_root, we need to make the mount units depend on
   146  		// all of the various initrd special targets by adding runtime conf
   147  		// files there
   148  		// note we could do this statically in the initramfs main filesystem
   149  		// layout, but that means that changes to snap-bootstrap would block on
   150  		// waiting for those files to be added before things works here, this is
   151  		// a more flexible strategy that puts snap-bootstrap in control
   152  		overrideContent := []byte(fmt.Sprintf(unitFileDependOverride, unitName))
   153  		for _, initrdUnit := range []string{
   154  			"initrd.target",
   155  			"initrd-fs.target",
   156  			"initrd-switch-root.target",
   157  			"local-fs.target",
   158  		} {
   159  			targetDir := filepath.Join(dirs.GlobalRootDir, "/run/systemd/system", initrdUnit+".d")
   160  			err := os.MkdirAll(targetDir, 0755)
   161  			if err != nil {
   162  				return err
   163  			}
   164  
   165  			// add an override file for the initrd unit to depend on this mount
   166  			// unit so that when we isolate to the initrd unit, it does not get
   167  			// unmounted
   168  			fname := fmt.Sprintf("snap_bootstrap_%s.conf", whereEscaped)
   169  			err = ioutil.WriteFile(filepath.Join(targetDir, fname), overrideContent, 0644)
   170  			if err != nil {
   171  				return err
   172  			}
   173  		}
   174  	}
   175  
   176  	if !opts.NoWait {
   177  		// TODO: is this necessary, systemd-mount seems to only return when the
   178  		// unit is active and the mount is there, but perhaps we should be a bit
   179  		// paranoid here and wait anyways?
   180  		// see systemd-mount(1)
   181  
   182  		// wait for the mount to exist
   183  		start := timeNow()
   184  		var now time.Time
   185  		for now = timeNow(); now.Sub(start) < defaultMountUnitWaitTimeout; now = timeNow() {
   186  			mounted, err := osutilIsMounted(where)
   187  			if mounted {
   188  				break
   189  			}
   190  			if err != nil {
   191  				return err
   192  			}
   193  		}
   194  
   195  		if now.Sub(start) > defaultMountUnitWaitTimeout {
   196  			return fmt.Errorf("timed out after %s waiting for mount %s on %s", defaultMountUnitWaitTimeout, what, where)
   197  		}
   198  	}
   199  
   200  	return nil
   201  }