code-intelligence.com/cifuzz@v0.40.0/pkg/minijail/minijail.go (about)

     1  package minijail
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"code-intelligence.com/cifuzz/pkg/log"
    13  	"code-intelligence.com/cifuzz/pkg/runfiles"
    14  	"code-intelligence.com/cifuzz/util/fileutil"
    15  )
    16  
    17  const (
    18  	// BindingsEnvVarName is an environment variable which users can
    19  	// use to specify additional Minijail bindings. The bindings must
    20  	// be separated by colon and can be specified in the same format
    21  	// that is supported by minijail's --bind-mount flag:
    22  	// <src>[,[dest][,<writeable>]], where <src> must be an absolute
    23  	// path and <writeable> is either 0 or 1.
    24  	BindingsEnvVarName = "CIFUZZ_MINIJAIL_BINDINGS"
    25  
    26  	// Mount flags as defined in golang.org/x/sys/unix. We're not using
    27  	// that package because it's not available on macOS.
    28  	MS_RDONLY      = 0x1       //nolint:all
    29  	MS_NOSUID      = 0x2       //nolint:all
    30  	MS_NODEV       = 0x4       //nolint:all
    31  	MS_BIND        = 0x1000    //nolint:all
    32  	MS_REC         = 0x4000    //nolint:all
    33  	MS_STRICTATIME = 0x1000000 //nolint:all
    34  )
    35  
    36  type WritableOption int
    37  
    38  const (
    39  	ReadOnly WritableOption = iota
    40  	ReadWrite
    41  )
    42  
    43  type Binding struct {
    44  	Source   string
    45  	Target   string
    46  	Writable WritableOption
    47  }
    48  
    49  func (b *Binding) String() string {
    50  	if b.Target == "" {
    51  		b.Target = b.Source
    52  	}
    53  	if b.Writable == ReadWrite {
    54  		return fmt.Sprintf("%s,%s,1", b.Source, b.Target)
    55  	}
    56  	// Don't use a short form if the source or target contain a comma,
    57  	// which would be interpreted as separators by minijail.
    58  	if strings.ContainsRune(b.Source, ',') || strings.ContainsRune(b.Target, ',') {
    59  		return fmt.Sprintf("%s,%s,0", b.Source, b.Target)
    60  	}
    61  	if b.Source != b.Target {
    62  		return fmt.Sprintf("%s,%s", b.Source, b.Target)
    63  	}
    64  	return b.Source
    65  }
    66  
    67  func BindingFromString(s string) (*Binding, error) {
    68  	tokens := strings.SplitN(s, ",", 3)
    69  	switch len(tokens) {
    70  	case 1:
    71  		return &Binding{Source: tokens[0], Target: tokens[0], Writable: 0}, nil
    72  	case 2:
    73  		return &Binding{Source: tokens[0], Target: tokens[1], Writable: 0}, nil
    74  	case 3:
    75  		writable, err := strconv.Atoi(tokens[2])
    76  		if err != nil {
    77  			return nil, errors.WithStack(err)
    78  		}
    79  		return &Binding{Source: tokens[0], Target: tokens[1], Writable: WritableOption(writable)}, nil
    80  	}
    81  	return nil, errors.Errorf("Bad binding: %s", s)
    82  }
    83  
    84  var fixedMinijailArgs = []string{
    85  	// Most of these args are the same as the ones clusterfuzz sets in
    86  	// their minijail wrapper:
    87  	// https://github.com/google/clusterfuzz/blob/4f8020c4c7ce73c1da0e68f04943af30bb5f0b32/src/clusterfuzz/_internal/system/minijail.py
    88  	//
    89  	"-U", "-m", // Quote from clusterfuzz:
    90  	// root (uid 0 in namespace) -> USER.
    91  	// The reason for this is that minijail does setresuid(0, 0, 0) before doing a
    92  	// chroot, which means uid 0 needs access to the chroot dir (owned by USER).
    93  	//
    94  	// Note that we also run fuzzers as uid 0 (but with no capabilities in
    95  	// permitted/effective/inherited sets which *should* mean there"s nothing
    96  	// special about it). This is because the uid running the fuzzer also need
    97  	// access to things owned by USER (fuzzer binaries, supporting files), and USER
    98  	// can only be mapped once.
    99  	"-M",      // Map current gid to root
   100  	"-c", "0", // drop all capabilities.
   101  	"-n", // no_new_privs
   102  	"-v", // mount namespace
   103  	"-p", // PID namespace
   104  	"-l", // IPC namespace
   105  	"-I", // Run jailed process as init.
   106  	// Mount the whole filesystem read-only. All paths which should be
   107  	// writable have to be added explicitly as read-write bindings.
   108  	"-k", "/,/,none," + strconv.Itoa(MS_RDONLY|MS_BIND|MS_REC),
   109  	// Mount a new procfs on /proc
   110  	"-k", "proc,/proc,proc," + strconv.Itoa(MS_RDONLY),
   111  	// Mount a new tmpfs on /dev/shm
   112  	"-k", "tmpfs,/dev/shm,tmpfs," + strconv.Itoa(MS_NOSUID|MS_NODEV|MS_STRICTATIME) + ",mode=1777",
   113  	// Applications generally assume that /tmp is writable, so we mount
   114  	// a tmpfs on /tmp.
   115  	// Note that this causes paths below /tmp which are printed by the
   116  	// application not being accessible on the host. The alternative
   117  	// would be to mount the /tmp from the host read-writable, but that
   118  	// could cause PID file collisions.
   119  	"-k", "tmpfs,/tmp,tmpfs," + strconv.Itoa(MS_NOSUID|MS_NODEV|MS_STRICTATIME) + ",mode=1777",
   120  	// Same as for /tmp, /run and /var/run should be writable
   121  	"-k", "tmpfs,/run,tmpfs," + strconv.Itoa(MS_NOSUID|MS_NODEV|MS_STRICTATIME) + ",mode=1777",
   122  	"-k", "tmpfs,/var/run,tmpfs," + strconv.Itoa(MS_NOSUID|MS_NODEV|MS_STRICTATIME) + ",mode=1777",
   123  	// Added by us, to log to stderr
   124  	"--logging=stderr",
   125  }
   126  
   127  var defaultBindings = []*Binding{
   128  	// We allow access to /dev/null and /dev/urandom because AFL needs
   129  	// access to them and some fuzz targets might as well (for example
   130  	// our lighttpd example fuzz target).
   131  	// They have to be mounted read-write, else minijail fails with
   132  	// libminijail[1]: cannot bind-remount: [...] Operation not permitted
   133  	{Source: "/dev/null", Writable: ReadWrite},
   134  	{Source: "/dev/urandom", Writable: ReadWrite},
   135  }
   136  
   137  type Options struct {
   138  	Args      []string
   139  	Bindings  []*Binding
   140  	OutputDir string
   141  }
   142  
   143  type minijail struct {
   144  	*Options
   145  	Args      []string
   146  	chrootDir string
   147  }
   148  
   149  func NewMinijail(opts *Options) (*minijail, error) {
   150  	// Evaluate symlinks in the executable path
   151  	path, err := filepath.EvalSymlinks(opts.Args[0])
   152  	if err != nil {
   153  		return nil, errors.WithStack(err)
   154  	}
   155  	opts.Args[0] = path
   156  
   157  	// --------------------------
   158  	// --- Create directories ---
   159  	// --------------------------
   160  	// Create chroot directory
   161  	chrootDir, err := os.MkdirTemp("", "minijail-chroot-")
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	// Create /tmp, /proc directories.
   167  	for _, dir := range []string{"/proc", "/tmp"} {
   168  		err = os.MkdirAll(filepath.Join(chrootDir, dir), 0o755)
   169  		if err != nil {
   170  			return nil, errors.WithStack(err)
   171  		}
   172  	}
   173  
   174  	// Create /dev/shm which is required to allow using shared memory
   175  	err = os.MkdirAll(filepath.Join(chrootDir, "dev", "shm"), 0o755)
   176  	if err != nil {
   177  		return nil, errors.WithStack(err)
   178  	}
   179  
   180  	// ----------------------------
   181  	// --- Set up minijail args ---
   182  	// ----------------------------
   183  	minijailPath, err := runfiles.Finder.Minijail0Path()
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	minijailArgs := append([]string{minijailPath}, fixedMinijailArgs...)
   188  
   189  	// This causes minijail to not use preload hooking, which
   190  	// allows us to run it without the libminijailpreload.so. That has
   191  	// two benefits:
   192  	// * We can use a statically built minijail0 binary, avoiding runtime
   193  	//   dependencies on libcap.
   194  	// * It avoids that minijail0 doesn't print error messages, which
   195  	//   happens when preloading is used.
   196  	//
   197  	// Note that (quoting the Minijail manual [1]): "some jailing can
   198  	// only be achieved from the process to which they will actually
   199  	// apply [via preloading]".
   200  	// [1] https://google.github.io/minijail/minijail0.1.html#implementation
   201  	//
   202  	// Since we don't use minijail for security but only for safety
   203  	// (i.e. we only want to protect against accidental damage done to
   204  	// the system, like the fuzz target accidentally deleting files or
   205  	// killing processes etc), it should be fine that the jailing is not
   206  	// perfect.
   207  	minijailArgs = append(minijailArgs, "-T", "static", "--ambient")
   208  
   209  	// Change root filesystem to the chroot directory. See pivot_root(2).
   210  	minijailArgs = append(minijailArgs, "-P", chrootDir)
   211  
   212  	// -----------------------
   213  	// --- Set up bindings ---
   214  	// -----------------------
   215  	bindings := append(opts.Bindings, defaultBindings...)
   216  
   217  	// Allow read-write access to the minijail output directory
   218  	if opts.OutputDir != "" {
   219  		bindings = append(bindings, &Binding{Source: opts.OutputDir, Writable: ReadWrite})
   220  	}
   221  
   222  	// We expect the current working directory to be the artifacts
   223  	// directory, which should be accessible to the fuzz target, so we
   224  	// add a binding for it.
   225  	// Some fuzz targets (e.g. the one for nginx) write to the working
   226  	// directory, which is why we mount it read-write. We decided that
   227  	// this is fine on CIFUZZ-1192.
   228  	workdir, err := os.Getwd()
   229  	if err != nil {
   230  		return nil, errors.WithStack(err)
   231  	}
   232  	bindings = append(bindings, &Binding{Source: workdir, Writable: ReadWrite})
   233  
   234  	// Add binding for the executable
   235  	bindings = append(bindings, &Binding{Source: path})
   236  
   237  	// Add binding for process_wrapper. process_wrapper changes the
   238  	// working directory and then executes the specified command.
   239  	processWrapperPath, err := runfiles.Finder.ProcessWrapperPath()
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	bindings = append(bindings, &Binding{Source: processWrapperPath})
   244  
   245  	// Add additional bindings from the environment variable
   246  	additionalBindingsEnv := os.Getenv(BindingsEnvVarName)
   247  	for _, s := range strings.Split(additionalBindingsEnv, ":") {
   248  		if s == "" {
   249  			continue
   250  		}
   251  		binding, err := BindingFromString(s)
   252  		if err != nil {
   253  			return nil, err
   254  		}
   255  
   256  		exists, err := fileutil.Exists(binding.Source)
   257  		if err != nil {
   258  			return nil, err
   259  		}
   260  		if !exists {
   261  			log.Debugf("Skipping binding %v: No such file or directory", binding.Source)
   262  			continue
   263  		}
   264  
   265  		log.Debugf("Adding binding %v", binding.Source)
   266  		bindings = append(bindings, binding)
   267  	}
   268  
   269  	// Create the bindings
   270  	for _, binding := range bindings {
   271  		if binding.Target == "" {
   272  			binding.Target = binding.Source
   273  		}
   274  		// Skip if the source doesn't exist
   275  		exists, err := fileutil.Exists(binding.Source)
   276  		if err != nil {
   277  			return nil, err
   278  		}
   279  		if !exists {
   280  			continue
   281  		}
   282  
   283  		// Create the destination
   284  		if fileutil.IsDir(binding.Source) {
   285  			err = os.MkdirAll(filepath.Join(chrootDir, binding.Target), 0o755)
   286  			if err != nil {
   287  				return nil, errors.WithStack(err)
   288  			}
   289  		} else {
   290  			err = os.MkdirAll(filepath.Join(chrootDir, filepath.Dir(binding.Target)), 0o755)
   291  			if err != nil {
   292  				return nil, errors.WithStack(err)
   293  			}
   294  			err = fileutil.Touch(filepath.Join(chrootDir, binding.Target))
   295  			if err != nil {
   296  				return nil, err
   297  			}
   298  		}
   299  
   300  		minijailArgs = append(minijailArgs, "-b", binding.String())
   301  	}
   302  
   303  	// -----------------------------------
   304  	// --- Set up process wrapper args ---
   305  	// -----------------------------------
   306  	// The process wrapper changes the working directory inside the
   307  	// sandbox to the first argument
   308  	processWrapperArgs := []string{processWrapperPath, workdir}
   309  
   310  	// --------------------
   311  	// --- Run minijail ---
   312  	// --------------------
   313  	args := append(minijailArgs, "--")
   314  	args = append(args, processWrapperArgs...)
   315  	args = append(args, opts.Args...)
   316  
   317  	// When DEBUG_MINIJAIL is set, we don't execute the actual libFuzzer
   318  	// command but only print it and start a shell instead. When used
   319  	// together with SKIP_CLEANUP, this allows to copy the Minijail
   320  	// command from the logs to open a shell in the sandbox environment
   321  	// to debug issues interactively.
   322  	if os.Getenv("DEBUG_MINIJAIL") != "" {
   323  		log.Print("libFuzzer command: ", strings.Join(opts.Args, " "))
   324  		args = append(minijailArgs, "--")
   325  		args = append(args, processWrapperArgs...)
   326  		args = append(args, "/bin/sh")
   327  	}
   328  
   329  	return &minijail{
   330  		Options:   opts,
   331  		chrootDir: chrootDir,
   332  		Args:      args,
   333  	}, nil
   334  }
   335  
   336  func (m *minijail) Cleanup() {
   337  	fileutil.Cleanup(m.chrootDir)
   338  }