github.com/criyle/go-sandbox@v0.10.3/cmd/runprog/main_darwin.go (about)

     1  // Command runprog executes program defined restricted environment including seccomp-ptraced, namespaced and containerized.
     2  package main
     3  
     4  import (
     5  	"flag"
     6  	"fmt"
     7  	"os"
     8  	"syscall"
     9  	"time"
    10  
    11  	"github.com/criyle/go-sandbox/pkg/forkexec"
    12  	"github.com/criyle/go-sandbox/pkg/rlimit"
    13  	"github.com/criyle/go-sandbox/runner"
    14  	"golang.org/x/sys/unix"
    15  )
    16  
    17  var (
    18  	timeLimit, realTimeLimit, memoryLimit, outputLimit, stackLimit uint64
    19  	inputFileName, outputFileName, errorFileName, workPath         string
    20  
    21  	profilePath, result string
    22  	showDetails         bool
    23  
    24  	args []string
    25  )
    26  
    27  func main() {
    28  	flag.Usage = printUsage
    29  	flag.Uint64Var(&timeLimit, "tl", 1, "Set time limit (in second)")
    30  	flag.Uint64Var(&realTimeLimit, "rtl", 0, "Set real time limit (in second)")
    31  	flag.Uint64Var(&memoryLimit, "ml", 256, "Set memory limit (in mb)")
    32  	flag.Uint64Var(&outputLimit, "ol", 64, "Set output limit (in mb)")
    33  	flag.Uint64Var(&stackLimit, "sl", 32, "Set stack limit (in mb)")
    34  	flag.StringVar(&inputFileName, "in", "", "Set input file name")
    35  	flag.StringVar(&outputFileName, "out", "", "Set output file name")
    36  	flag.StringVar(&errorFileName, "err", "", "Set error file name")
    37  	flag.StringVar(&workPath, "work-path", "", "Set the work path of the program")
    38  	flag.StringVar(&profilePath, "p", "", "sandbox profile")
    39  	flag.BoolVar(&showDetails, "show-trace-details", false, "Show trace details")
    40  	flag.StringVar(&result, "res", "stdout", "Set the file name for output the result")
    41  	flag.Parse()
    42  
    43  	args = flag.Args()
    44  	if len(args) == 0 {
    45  		printUsage()
    46  	}
    47  
    48  	if realTimeLimit < timeLimit {
    49  		realTimeLimit = timeLimit + 2
    50  	}
    51  	if stackLimit > memoryLimit {
    52  		stackLimit = memoryLimit
    53  	}
    54  	if workPath == "" {
    55  		workPath, _ = os.Getwd()
    56  	}
    57  
    58  	var (
    59  		f   *os.File
    60  		err error
    61  	)
    62  	if result == "stdout" {
    63  		f = os.Stdout
    64  	} else if result == "stderr" {
    65  		f = os.Stderr
    66  	} else {
    67  		f, err = os.Create(result)
    68  		if err != nil {
    69  			debug("Failed to open result file:", err)
    70  			return
    71  		}
    72  		defer f.Close()
    73  	}
    74  
    75  	rt, err := start()
    76  	debug(rt, err)
    77  	if e, ok := err.(syscall.Errno); ok {
    78  		debug("errno", int(e))
    79  	}
    80  
    81  	if rt == nil {
    82  		rt = &runner.Result{
    83  			Status: runner.StatusRunnerError,
    84  		}
    85  	}
    86  	if err == nil && rt.Status != runner.StatusNormal {
    87  		err = rt.Status
    88  	}
    89  	debug("setupTime: ", rt.SetUpTime)
    90  	debug("runningTime: ", rt.RunningTime)
    91  	if err != nil {
    92  		debug(err)
    93  		c, ok := err.(runner.Status)
    94  		if !ok {
    95  			c = runner.StatusRunnerError
    96  		}
    97  		// Handle fatal error from trace
    98  		fmt.Fprintf(f, "%d %d %d %d\n", getStatus(c), int(rt.Time/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
    99  		if c == runner.StatusRunnerError {
   100  			os.Exit(1)
   101  		}
   102  	} else {
   103  		fmt.Fprintf(f, "%d %d %d %d\n", 0, int(rt.Time/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
   104  	}
   105  }
   106  
   107  func start() (*runner.Result, error) {
   108  	var sTime, mTime, fTime time.Time
   109  	sTime = time.Now()
   110  	files, err := prepareFiles(inputFileName, outputFileName, errorFileName)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	defer closeFiles(files)
   115  
   116  	var profile string
   117  	if profilePath != "" {
   118  		c, err := os.ReadFile(profilePath)
   119  		if err != nil {
   120  			return nil, fmt.Errorf("profile: %v", err)
   121  		}
   122  		profile = string(c)
   123  	}
   124  
   125  	// if not defined, then use the original value
   126  	fds := make([]uintptr, len(files))
   127  	for i, f := range files {
   128  		if f != nil {
   129  			fds[i] = f.Fd()
   130  		} else {
   131  			fds[i] = uintptr(i)
   132  		}
   133  	}
   134  
   135  	rlims := rlimit.RLimits{
   136  		CPU:          timeLimit,
   137  		CPUHard:      realTimeLimit,
   138  		FileSize:     outputLimit << 20,
   139  		Data:         memoryLimit << 20,
   140  		AddressSpace: memoryLimit << 20,
   141  		Stack:        stackLimit << 20,
   142  	}
   143  
   144  	debug(rlims)
   145  	debug(args)
   146  
   147  	r := forkexec.Runner{
   148  		Args:           args,
   149  		Env:            []string{pathEnv},
   150  		RLimits:        rlims.PrepareRLimit(),
   151  		Files:          fds,
   152  		WorkDir:        workPath,
   153  		SandboxProfile: profile,
   154  		SyncFunc: func(pid int) error {
   155  			mTime = time.Now()
   156  			return nil
   157  		},
   158  	}
   159  	pid, err := r.Start()
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	defer func() {
   165  		killAll(pid)
   166  		collectZombie(pid)
   167  	}()
   168  
   169  	var (
   170  		wstatus syscall.WaitStatus
   171  		rusage  syscall.Rusage
   172  	)
   173  	for {
   174  		_, err = syscall.Wait4(pid, &wstatus, 0, &rusage)
   175  		if err == syscall.EINTR {
   176  			continue
   177  		}
   178  		fTime = time.Now()
   179  		if err != nil {
   180  			return nil, err
   181  		}
   182  		result := runner.Result{
   183  			Status:      runner.StatusNormal,
   184  			Time:        time.Duration(rusage.Utime.Nano()),
   185  			Memory:      runner.Size(rusage.Maxrss), // seems MacOS uses bytes instead of kb
   186  			SetUpTime:   mTime.Sub(sTime),
   187  			RunningTime: fTime.Sub(mTime),
   188  		}
   189  		if uint64(result.Time) > timeLimit*1e9 {
   190  			result.Status = runner.StatusTimeLimitExceeded
   191  		}
   192  		if uint64(result.Memory) > memoryLimit<<20 {
   193  			result.Status = runner.StatusMemoryLimitExceeded
   194  		}
   195  
   196  		switch {
   197  		case wstatus.Exited():
   198  			if status := wstatus.ExitStatus(); status != 0 {
   199  				result.Status = runner.StatusNonzeroExitStatus
   200  			}
   201  			return &result, nil
   202  
   203  		case wstatus.Signaled():
   204  			sig := wstatus.Signal()
   205  			switch sig {
   206  			case unix.SIGXCPU, unix.SIGKILL:
   207  				result.Status = runner.StatusTimeLimitExceeded
   208  			case unix.SIGXFSZ:
   209  				result.Status = runner.StatusOutputLimitExceeded
   210  			case unix.SIGSYS:
   211  				result.Status = runner.StatusDisallowedSyscall
   212  			default:
   213  				result.Status = runner.StatusSignalled
   214  			}
   215  			result.ExitStatus = int(sig)
   216  			return &result, nil
   217  		}
   218  	}
   219  }
   220  
   221  // kill all tracee according to pids
   222  func killAll(pgid int) {
   223  	unix.Kill(-pgid, unix.SIGKILL)
   224  }
   225  
   226  // collect died child processes
   227  func collectZombie(pgid int) {
   228  	var wstatus unix.WaitStatus
   229  	for {
   230  		if _, err := unix.Wait4(-pgid, &wstatus, unix.WNOHANG, nil); err != unix.EINTR && err != nil {
   231  			break
   232  		}
   233  	}
   234  }