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 }