github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/cmd/podman-mac-helper/install.go (about)

     1  //go:build darwin
     2  // +build darwin
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"io/fs"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"syscall"
    15  	"text/template"
    16  
    17  	"github.com/pkg/errors"
    18  	"github.com/spf13/cobra"
    19  )
    20  
    21  const (
    22  	rwx_rx_rx = 0755
    23  	rw_r_r    = 0644
    24  )
    25  
    26  const launchConfig = `<?xml version="1.0" encoding="UTF-8"?>
    27  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    28  <plist version="1.0">
    29  <dict>
    30  	<key>Label</key>
    31  	<string>com.github.containers.podman.helper-{{.User}}</string>
    32  	<key>ProgramArguments</key>
    33  	<array>
    34  		<string>{{.Program}}</string>
    35  		<string>service</string>
    36  		<string>{{.Target}}</string>
    37  	</array>
    38  	<key>inetdCompatibility</key>
    39  	<dict>
    40  		<key>Wait</key>
    41  		<false/>
    42  	</dict>
    43  	<key>UserName</key>
    44  	<string>root</string>
    45  	<key>Sockets</key>
    46  	<dict>
    47  		<key>Listeners</key>
    48  		<dict>
    49  			<key>SockFamily</key>
    50  			<string>Unix</string>
    51  			<key>SockPathName</key>
    52  			<string>/private/var/run/podman-helper-{{.User}}.socket</string>
    53  			<key>SockPathOwner</key>
    54  			<integer>{{.UID}}</integer>
    55  			<key>SockPathMode</key>
    56  			<!-- SockPathMode takes base 10 (384 = 0600) -->
    57  			<integer>384</integer>
    58  			<key>SockType</key>
    59  			<string>stream</string>
    60  		</dict>
    61  	</dict>
    62  </dict>
    63  </plist>
    64  `
    65  
    66  type launchParams struct {
    67  	Program string
    68  	User    string
    69  	UID     string
    70  	Target  string
    71  }
    72  
    73  var installCmd = &cobra.Command{
    74  	Use:    "install",
    75  	Short:  "installs the podman helper agent",
    76  	Long:   "installs the podman helper agent, which manages the /var/run/docker.sock link",
    77  	PreRun: silentUsage,
    78  	RunE:   install,
    79  }
    80  
    81  func init() {
    82  	addPrefixFlag(installCmd)
    83  	rootCmd.AddCommand(installCmd)
    84  }
    85  
    86  func install(cmd *cobra.Command, args []string) error {
    87  	userName, uid, homeDir, err := getUser()
    88  	if err != nil {
    89  		return err
    90  	}
    91  
    92  	labelName := fmt.Sprintf("com.github.containers.podman.helper-%s.plist", userName)
    93  	fileName := filepath.Join("/Library", "LaunchDaemons", labelName)
    94  
    95  	if _, err := os.Stat(fileName); err == nil || !os.IsNotExist(err) {
    96  		return errors.New("helper is already installed, uninstall first")
    97  	}
    98  
    99  	prog, err := installExecutable(userName)
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	target := filepath.Join(homeDir, ".local", "share", "containers", "podman", "machine", "podman.sock")
   105  	var buf bytes.Buffer
   106  	t := template.Must(template.New("launchdConfig").Parse(launchConfig))
   107  	err = t.Execute(&buf, launchParams{prog, userName, uid, target})
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, rw_r_r)
   113  	if err != nil {
   114  		return errors.Wrap(err, "error creating helper plist file")
   115  	}
   116  	defer file.Close()
   117  	_, err = buf.WriteTo(file)
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	if err = runDetectErr("launchctl", "load", fileName); err != nil {
   123  		return errors.Wrap(err, "launchctl failed loading service")
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  func restrictRecursive(targetDir string, until string) error {
   130  	for targetDir != until && len(targetDir) > 1 {
   131  		info, err := os.Lstat(targetDir)
   132  		if err != nil {
   133  			return err
   134  		}
   135  		if info.Mode()&fs.ModeSymlink != 0 {
   136  			return errors.Errorf("symlinks not allowed in helper paths (remove them and rerun): %s", targetDir)
   137  		}
   138  		if err = os.Chown(targetDir, 0, 0); err != nil {
   139  			return errors.Wrap(err, "could not update ownership of helper path")
   140  		}
   141  		if err = os.Chmod(targetDir, rwx_rx_rx|fs.ModeSticky); err != nil {
   142  			return errors.Wrap(err, "could not update permissions of helper path")
   143  		}
   144  		targetDir = filepath.Dir(targetDir)
   145  	}
   146  
   147  	return nil
   148  }
   149  
   150  func verifyRootDeep(path string) error {
   151  	path = filepath.Clean(path)
   152  	current := "/"
   153  	segs := strings.Split(path, "/")
   154  	depth := 0
   155  	for i := 1; i < len(segs); i++ {
   156  		seg := segs[i]
   157  		current = filepath.Join(current, seg)
   158  		info, err := os.Lstat(current)
   159  		if err != nil {
   160  			return err
   161  		}
   162  
   163  		stat := info.Sys().(*syscall.Stat_t)
   164  		if stat.Uid != 0 {
   165  			return errors.Errorf("installation target path must be solely owned by root: %s is not", current)
   166  		}
   167  
   168  		if info.Mode()&fs.ModeSymlink != 0 {
   169  			target, err := os.Readlink(current)
   170  			if err != nil {
   171  				return err
   172  			}
   173  
   174  			targetParts := strings.Split(target, "/")
   175  			segs = append(targetParts, segs[i+1:]...)
   176  
   177  			if depth++; depth > 1000 {
   178  				return errors.New("reached max recursion depth, link structure is cyclical or too complex")
   179  			}
   180  
   181  			if !filepath.IsAbs(target) {
   182  				current = filepath.Dir(current)
   183  				i = -1 // Start at 0
   184  			} else {
   185  				current = "/"
   186  				i = 0 // Skip empty first segment
   187  			}
   188  		}
   189  	}
   190  
   191  	return nil
   192  }
   193  
   194  func installExecutable(user string) (string, error) {
   195  	// Since the installed executable runs as root, as a precaution verify root ownership of
   196  	// the entire installation path, and utilize sticky + read only perms for the helper path
   197  	// suffix. The goal is to help users harden against privilege escalation from loose
   198  	// filesystem permissions.
   199  	//
   200  	// Since userspace package management tools, such as brew, delegate management of system
   201  	// paths to standard unix users, the daemon executable is copied into a separate more
   202  	// restricted area of the filesystem.
   203  	if err := verifyRootDeep(installPrefix); err != nil {
   204  		return "", err
   205  	}
   206  
   207  	targetDir := filepath.Join(installPrefix, "podman", "helper", user)
   208  	if err := os.MkdirAll(targetDir, rwx_rx_rx); err != nil {
   209  		return "", errors.Wrap(err, "could not create helper directory structure")
   210  	}
   211  
   212  	// Correct any incorrect perms on previously existing directories and verify no symlinks
   213  	if err := restrictRecursive(targetDir, installPrefix); err != nil {
   214  		return "", err
   215  	}
   216  
   217  	exec, err := os.Executable()
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  	install := filepath.Join(targetDir, filepath.Base(exec))
   222  
   223  	return install, copyFile(install, exec, rwx_rx_rx)
   224  }
   225  
   226  func copyFile(dest string, source string, perms fs.FileMode) error {
   227  	in, err := os.Open(source)
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	defer in.Close()
   233  	out, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	defer out.Close()
   239  	if _, err := io.Copy(out, in); err != nil {
   240  		return err
   241  	}
   242  
   243  	return nil
   244  }