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