github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/cmds/exp/pox/pox.go (about)

     1  // Copyright 2012-2021 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // pox packages dynamic executable into an archive.
     6  //
     7  // Synopsis:
     8  //
     9  //	pox [-[-verbose]|v] [-[-file]|f tcz-file] -[-create]|c FILE [...FILE]
    10  //	pox [-[-verbose]|v] [-[-file]|f tcz-file] -[-run|r] PROGRAM -- [...ARGS]
    11  //	pox [-[-verbose]|v] [-[-file]|f tcz-file] -[-create]|c -[-run|r] PROGRAM -- [...ARGS]
    12  //
    13  // Description:
    14  //
    15  //	pox packages a dynamic executable into an archive for use on another
    16  //	machine. By default, it uses the tcz format compatible with tinycore.
    17  //
    18  //	pox supports 3 archive formats:
    19  //	1) squashfs (default): The tcz is a squashfs. This requires mksquashfs.
    20  //	2) zip
    21  //	3) elf+zip: Self-extracting.
    22  //
    23  // Options:
    24  //
    25  //	create|c: create the TCZ file.
    26  //	verbose|d: verbose
    27  //	file|f FILE: file name (default /tmp/pox.tcz)
    28  //	run|r: Runs the first non-flag argument to pox.  Remaining arguments will
    29  //	       be passed to the program.  Use '--' before any flag-like arguments
    30  //	       to prevent pox from interpretting the flags.
    31  //	self|s: Create a self-extracting elf. This implies -z.
    32  //	zip|z: Use zip and unzip instead of a loopback mounted squashfs. Be sure
    33  //	       to use -z for both creation and running, or not at all.
    34  //	For convenience and testing, you can create and run a pox in one command.
    35  //
    36  // Example:
    37  //
    38  //	$ pox -c /bin/bash /bin/cat /bin/ls /etc/hosts
    39  //	Will build a squashfs, which will be /tmp/pox.tcz
    40  //
    41  //	$ sudo pox -r /bin/bash
    42  //	Will drop you into the /tmp/pox.tcz running bash
    43  //	You can use ls and cat on /etc/hosts.
    44  //
    45  //	Simpler example, with arguments:
    46  //	$ sudo pox -r /bin/ls -- -la
    47  //	will run `ls -la` and exit.
    48  //
    49  //	$ sudo pox -r -- /bin/ls -la
    50  //	Syntactically easier: the program name can come after '--'
    51  //
    52  //	$ sudo pox -c -r /bin/bash
    53  //	Create a pox with a bash and run it.
    54  //
    55  //	$ pox -cvsf date /bin/date
    56  //	Creates a self-executing pox called "date".
    57  //	$ ./date --utc
    58  //
    59  // Notes:
    60  //
    61  //   - When running a pox, you likely need sudo to chroot
    62  //
    63  //   - Binaries run out of a chroot often need files you are unaware of. For
    64  //     instance, if bash can't find terminfo files, it won't know to handle
    65  //     backspaces properly. (They occur, but are not shown). To fix this, pass
    66  //     pox all of the files you need.  For bash: `find /lib/terminfo -type f`.
    67  //
    68  //   - Other programs rely on helper functions, such as '/bin/man'. If your
    69  //     program has built-in help commands that trigger man pages, e.g. "git
    70  //     help foo", you'll want to include /bin/man too. But you'll also need
    71  //     everything that man uses, such as /etc/manpath.config. My advice: skip
    72  //     it.
    73  //
    74  //   - When adding all files in a directory, the easiest thing to do is:
    75  //     `find $DIR -type f` (Note the ticks: this is a bash command execution).
    76  //
    77  //   - When creating a pox with an executable with shared libraries that are
    78  //     not installed on your system, such as for a project installed in your
    79  //     home directory, run pox from the installation prefix directory, such
    80  //     that the lib/ and bin/ are in pox's working directory. Pox will strip
    81  //     its working directory from the paths of the files it builds. Having bin/
    82  //     in the root of the pox file helps with PATH lookups, and not having the
    83  //     full path from your machine in the pox file makes it easier to extract a
    84  //     pox file to /usr/local/.
    85  //
    86  //   - pox is not a security boundary. chroot is well known to have holes.
    87  //     Pox is about enabling execution. Don't expect it to "wall things off".
    88  //     In fact, we mount /dev, /proc, and /sys; and you can add more things.
    89  //     Commands run under pox are just as dangerous as anything else.
    90  package main
    91  
    92  import (
    93  	"errors"
    94  	"fmt"
    95  	"io"
    96  	"log"
    97  	"os"
    98  	"os/exec"
    99  	"path/filepath"
   100  	"sort"
   101  	"strings"
   102  	"syscall"
   103  
   104  	flag "github.com/spf13/pflag"
   105  	"github.com/mvdan/u-root-coreutils/pkg/cp"
   106  	"github.com/mvdan/u-root-coreutils/pkg/ldd"
   107  	"github.com/mvdan/u-root-coreutils/pkg/mount"
   108  	"github.com/mvdan/u-root-coreutils/pkg/mount/loop"
   109  	"github.com/mvdan/u-root-coreutils/pkg/uzip"
   110  )
   111  
   112  const usage = "pox [-[-verbose]|v] -[-run|r] | -[-create]|c  [-[-file]|f tcz-file] file [...file]"
   113  
   114  type mp struct {
   115  	source string
   116  	target string
   117  	fstype string
   118  	flags  uintptr
   119  	data   string
   120  	perm   os.FileMode // for target in the chroot
   121  }
   122  
   123  var (
   124  	verbose = flag.BoolP("verbose", "v", false, "enable verbose prints")
   125  	run     = flag.BoolP("run", "r", false, "Run the first file argument")
   126  	create  = flag.BoolP("create", "c", false, "create it")
   127  	zip     = flag.BoolP("zip", "z", false, "use zip instead of squashfs")
   128  	self    = flag.BoolP("self", "s", false, "use self-extracting zip")
   129  	file    = flag.StringP("output", "f", "/tmp/pox.tcz", "Output file")
   130  	extra   = flag.StringP("extra", "e", "", `comma-separated list of extra directories to add (on create) and binds to do (on run).
   131  You can specify what directories to add, and when you run, specify what directories are bound over them, e.g.:
   132  pox -c -e /tmp,/etc commands ....
   133  pox -r -e /a/b/c/tmp:/tmp,/etc:/etc commands ...
   134  This can also be passed in with the POX_EXTRA variable.
   135  `)
   136  	v = func(string, ...interface{}) {}
   137  )
   138  
   139  // When chrooting, programs often want to access various system directories:
   140  var chrootMounts = []mp{
   141  	// mount --bind /sys /chroot/sys
   142  	{"/sys", "/sys", "", mount.MS_BIND, "", 0o555},
   143  	// mount -t proc /proc /chroot/proc
   144  	{"/proc", "/proc", "proc", 0, "", 0o555},
   145  	// mount --bind /dev /chroot/dev
   146  	{"/dev", "/dev", "", mount.MS_BIND, "", 0o755}}
   147  
   148  func poxCreate(bin ...string) error {
   149  	if len(bin) == 0 {
   150  		return fmt.Errorf(usage)
   151  	}
   152  	l, err := ldd.Ldd(bin)
   153  	if err != nil {
   154  		var stderr []byte
   155  		if eerr, ok := err.(*exec.ExitError); ok {
   156  			stderr = eerr.Stderr
   157  		}
   158  		return fmt.Errorf("running ldd on %v: %v %s", bin, err, stderr)
   159  	}
   160  
   161  	var names []string
   162  	for _, dep := range l {
   163  		names = append(names, dep.FullName)
   164  	}
   165  	sort.Strings(names)
   166  	// Now we need to make a template file hierarchy and put
   167  	// the stuff we want in there.
   168  	dir, err := os.MkdirTemp("", "pox")
   169  	if err != nil {
   170  		return err
   171  	}
   172  	defer os.RemoveAll(dir)
   173  	pwd, err := os.Getwd()
   174  	if err != nil {
   175  		return err
   176  	}
   177  	// We don't use defer() here to close files as
   178  	// that can cause open failures with a large enough number.
   179  	for _, f := range names {
   180  		v("Adding %q", f)
   181  		fi, err := os.Stat(f)
   182  		if err != nil {
   183  			return err
   184  		}
   185  		in, err := os.Open(f)
   186  		if err != nil {
   187  			return err
   188  		}
   189  		f = strings.TrimPrefix(f, pwd)
   190  		dfile := filepath.Join(dir, f)
   191  		d := filepath.Dir(dfile)
   192  		if err := os.MkdirAll(d, 0o755); err != nil {
   193  			in.Close()
   194  			return err
   195  		}
   196  		out, err := os.OpenFile(dfile, os.O_WRONLY|os.O_CREATE,
   197  			fi.Mode().Perm())
   198  		if err != nil {
   199  			in.Close()
   200  			return err
   201  		}
   202  		_, err = io.Copy(out, in)
   203  		in.Close()
   204  		out.Close()
   205  		if err != nil {
   206  			return err
   207  		}
   208  	}
   209  
   210  	for _, m := range chrootMounts {
   211  		d := filepath.Join(dir, m.target)
   212  		v("Adding mount %q, perm %s", d, m.perm.String())
   213  		if err := os.MkdirAll(d, m.perm); err != nil {
   214  			return err
   215  		}
   216  	}
   217  	if err := os.Remove(*file); err != nil && !os.IsNotExist(err) {
   218  		return err
   219  	}
   220  
   221  	if *self {
   222  		// Make a copy of the exe and append the zip file.
   223  		exe, err := os.Executable()
   224  		if err != nil {
   225  			return err
   226  		}
   227  		if err := cp.Copy(exe, *file); err != nil {
   228  			return err
   229  		}
   230  		if err := uzip.AppendZip(dir, *file, bin[0]); err != nil {
   231  			return err
   232  		}
   233  	} else if *zip {
   234  		if err := uzip.ToZip(dir, *file, ""); err != nil {
   235  			return err
   236  		}
   237  	} else {
   238  		c := exec.Command("mksquashfs", dir, *file, "-noappend")
   239  		o, err := c.CombinedOutput()
   240  		v("%v", string(o))
   241  		if err != nil {
   242  			return fmt.Errorf("%v: %v: %v", c.Args, string(o), err)
   243  		}
   244  	}
   245  
   246  	v("Done, your pox is %q", *file)
   247  	return nil
   248  }
   249  
   250  func poxRun(args ...string) error {
   251  	if len(args) == 0 {
   252  		return fmt.Errorf(usage)
   253  	}
   254  	dir, err := os.MkdirTemp("", "pox")
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	if *zip {
   260  		if err := uzip.FromZip(*file, dir); err != nil {
   261  			return err
   262  		}
   263  	} else {
   264  		lo, err := loop.New(*file, "squashfs", "")
   265  		if err != nil {
   266  			return err
   267  		}
   268  		defer lo.Free() //nolint:errcheck
   269  
   270  		mountPoint, err := lo.Mount(dir, 0)
   271  		if err != nil {
   272  			return err
   273  		}
   274  		defer mountPoint.Unmount(0) //nolint:errcheck
   275  	}
   276  	for _, m := range chrootMounts {
   277  		v("mount(%q, %q, %q, %q, %#x)", m.source, filepath.Join(dir, m.target), m.fstype, m.data, m.flags)
   278  		mp, err := mount.Mount(m.source, filepath.Join(dir, m.target), m.fstype, m.data, m.flags)
   279  		if err != nil {
   280  			return err
   281  		}
   282  		defer mp.Unmount(0) //nolint:errcheck
   283  	}
   284  
   285  	// If you pass Command a path with no slashes, it'll use PATH from the
   286  	// parent to resolve the path to exec.  Once we chroot, whatever path we
   287  	// picked is undoubtably wrong.  Let's help them out: if they give us a
   288  	// program with no /, let's look in /bin/.  If they want the root of the
   289  	// chroot, they can use "./"
   290  	if filepath.Base(args[0]) == args[0] {
   291  		args[0] = filepath.Join(string(os.PathSeparator), "bin", args[0])
   292  	}
   293  	c := exec.Command(args[0], args[1:]...)
   294  	c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr
   295  	c.SysProcAttr = &syscall.SysProcAttr{
   296  		Chroot: dir,
   297  	}
   298  	c.Env = append(os.Environ(), "PWD=.")
   299  
   300  	if err = c.Run(); err != nil {
   301  		v("pox command exited with: %v", err)
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  func extraMounts(mountList string) error {
   308  	if mountList == "" {
   309  		return nil
   310  	}
   311  	v("Extra mounts: %q", mountList)
   312  	// We have to specify the extra directories and do the create here b/c it is a squashfs. Sorry.
   313  	for _, e := range strings.Split(mountList, ",") {
   314  		m := mp{flags: mount.MS_BIND, perm: 0o755}
   315  		mp := strings.Split(e, ":")
   316  		switch len(mp) {
   317  		case 1:
   318  			m.source, m.target = mp[0], mp[0]
   319  		case 2:
   320  			m.source, m.target = mp[0], mp[1]
   321  		default:
   322  			return fmt.Errorf("%q is not in the form src:target", mp)
   323  		}
   324  		v("Extra mounts: append %q to chrootMounts", m)
   325  		chrootMounts = append(chrootMounts, m)
   326  	}
   327  	return nil
   328  }
   329  
   330  func pox(args ...string) error {
   331  	// If the current executable is a zip file, extract and run.
   332  	// Sneakily re-write os.Args to include a "-rzf" before flag parsing.
   333  	// The zip comment contains the executable path once extracted.
   334  	exe, err := os.Executable()
   335  	if err != nil {
   336  		return err
   337  	}
   338  	if comment, err := uzip.Comment(exe); err == nil {
   339  		if comment == "" {
   340  			return errors.New("expected zip comment on self-extracting pox")
   341  		}
   342  		os.Args = append([]string{
   343  			os.Args[0],
   344  			"-rzf", exe,
   345  			"--",
   346  			comment,
   347  		}, os.Args[1:]...)
   348  	}
   349  
   350  	if *verbose {
   351  		v = log.Printf
   352  	}
   353  	if err := extraMounts(*extra); err != nil {
   354  		return err
   355  	}
   356  	if err := extraMounts(os.Getenv("POX_EXTRA")); err != nil {
   357  		return err
   358  	}
   359  	if !*create && !*run {
   360  		return fmt.Errorf(usage)
   361  	}
   362  	if *create {
   363  		if err := poxCreate(args...); err != nil {
   364  			return err
   365  		}
   366  	}
   367  	if *run {
   368  		return poxRun(args...)
   369  	}
   370  	return nil
   371  }
   372  
   373  func main() {
   374  	flag.Parse()
   375  	if err := pox(flag.Args()...); err != nil {
   376  		log.Fatal(err)
   377  	}
   378  }