Compare commits

...

19 Commits

Author SHA1 Message Date
Gorgeous-Patrick
de10456275
Merge 369229e0d5 into 2ba894ffd1 2025-02-26 18:46:26 +08:00
criyle
2ba894ffd1 vfork: fix asm on other architectures
doc: update

#13
2025-02-22 21:30:33 +00:00
criyle
10235abbf2 container: organize test cases 2025-02-22 21:07:11 +00:00
criyle
396ac9723e forkexec: support vfork
(#13)
2025-02-22 20:04:15 +00:00
criyle
f9deb2dc1a container: add unshare cgroup before exec
#13
2025-02-20 23:09:50 -05:00
criyle
f0dd44f466 container: fix cgroup fd
forkexec: disabled VFORK for now as it is not working properly
#13
2025-02-20 22:52:26 -05:00
criyle
659099a2c0 forkexec: use vfork if no sync needed
#13
2025-02-20 22:35:09 +00:00
criyle
f8361a08a0 forkexec: only sync when syncFunc exists
#13
2025-02-20 21:45:03 +00:00
criyle
d32acd7591 cgroup&container: add ability to utilize cgroup fd with clone3
#13
2025-02-20 20:29:20 +00:00
criyle
f15b953065 container: syncAfterExec to sync after execve rather than before 2025-02-20 19:48:39 +00:00
criyle
3d8333e952 forkexec: initial support for clone3 2025-02-20 03:56:49 -05:00
criyle
f6f057edf9 cgroup: use rmdir to delete cgroup 2025-02-20 04:19:59 +00:00
criyle
51b53cf60b runner: add procPeak as measurement of peak process count 2025-02-20 03:12:28 +00:00
criyle
cf936ae31f build(deps): update dependencies and go 1.23 2025-02-20 02:04:22 +00:00
criyle
0b6b557947 cgroup: add pids.peak for cgroup v2 kernel >= 6.1 2025-02-20 01:51:59 +00:00
criyle
4154f44d83 forkexec: add more benchmark for std fork 2025-02-19 00:03:36 -05:00
Patrick Li
369229e0d5 fix: allow more essential syscalls 2024-12-05 18:36:33 -05:00
Gorgeous-Patrick
d58b2485a2 test: makefile for test 2024-12-05 14:56:57 -05:00
Gorgeous-Patrick
6407c89d90 DOCKER NOT WORKING 2024-12-05 14:47:05 -05:00
36 changed files with 672 additions and 184 deletions

3
.gitignore vendored
View File

@ -2,7 +2,6 @@
.DS_Store
# Test Env
test*/
env*.sh
# Test Files
@ -10,3 +9,5 @@ env*.sh
/test
.vscode
runprog
*.test

7
Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM golang
COPY . /app
WORKDIR /app/cmd/runprog
RUN apt update && apt install -y gcc libstdc++6 libseccomp-dev
RUN go build

View File

@ -13,8 +13,9 @@ Notice: Only works on Linux since ptrace, unshare, cgroup are available only on
## Build & Install
- install latest go compiler from [golang/download](https://golang.org/dl/)
- install libseccomp library: (for Ubuntu) `apt install libseccomp-dev`
- build & install: `go install github.com/criyle/go-sandbox/...`
- download repository: `git clone githuc.com/criyle/go-sandbox`
- build: `go build ./cmd/runprog`
- or install directly: `go install github.com/criyle/go-sandbox/cmd/runprog@latest`
## Technologies
@ -45,6 +46,12 @@ Default file access syscall check:
2. Use Linux Control Groups to limit & acct CPU & memory (eliminated wait4.rusage)
3. Container tech with execveat memfd, sethostname, setdomainname
### prefork containers
Utilize the linux namespace + cgroup but create container in advance to reduce the duplicated effort of creating mount points. See Pre-forked container protocol and environment for design details.
On kernel >= 5.7 with cgroup v2, the new `clone3(CLONE_INTO_CGROUP)` with `vfork` is available to reduce the resource consumption of create new address spaces as well.
## Design
### Result Status
@ -183,8 +190,11 @@ type Environment interface {
## Kernel Versions
- 6.1: `pids.peak` in cgroup v2
- 5.19: `memory.peak` in cgroup v2
- 4.15: cgroup v2
- 5.7: `clone3` with `CLONE_INTO_CGROUP`
- 5.3: `clone3`
- 4.15: cgroup v2 (also need support in the Linux distribution)
- 4.14: SECCOMP_RET_KILL_PROCESS
- 4.6: CLONE_NEWCGROUP
- 3.19: execveat()

View File

@ -16,6 +16,10 @@ var (
"/dev/urandom",
"/proc/meminfo",
"/etc/localtime",
"/usr/lib/libstdc++.so.6",
"/usr/lib/libc.so.6",
"/usr/lib/libm.so.6",
"/usr/lib/libgcc_s.so.1",
}
// default write permission files
@ -75,6 +79,17 @@ var (
"clock_gettime",
"restart_syscall",
"futex",
"gettid",
"getpid",
"prlimit64",
"getrandom",
"set_tid_address",
"set_robust_list",
"rseq",
"newfstatat",
}
// default syscalls to trace
@ -108,71 +123,11 @@ var (
// config for different type of program
// workpath and arg0 have additional read / stat permission
runptraceConfig = map[string]ProgramConfig{
"python2.7": {
Syscall: SyscallConfig{
ExtraAllow: []string{
"futex", "getdents", "getdents64", "prlimit64", "getpid", "sysinfo",
},
ExtraCount: map[string]int{
"set_tid_address": 1,
"set_robust_list": 1,
},
},
FileAccess: FileAccessConfig{
ExtraRead: []string{
"/usr/bin/python2.7",
"/usr/lib/python2.7/",
"/usr/bin/lib/python2.7/",
"/usr/local/lib/python2.7/",
"/usr/lib/pymodules/python2.7/",
"/usr/bin/Modules/",
"/usr/bin/pybuilddir.txt",
"/usr/lib/locale/",
"./answer.code",
},
ExtraStat: []string{
"/usr", "/usr/bin",
},
},
RunCommand: []string{"/usr/bin/python2.7", "-E", "-s", "-B"},
},
"python3": {
Syscall: SyscallConfig{
ExtraAllow: []string{
"futex", "getdents", "getdents64", "prlimit64", "getpid", "sysinfo", "getrandom",
},
ExtraCount: map[string]int{
"set_tid_address": 1,
"set_robust_list": 1,
},
},
FileAccess: FileAccessConfig{
ExtraRead: []string{
"/usr/bin/python3",
"/usr/lib/python3/",
"/usr/bin/python3.6",
"/usr/lib/python3.6/",
"/usr/bin/lib/python3.6/",
"/usr/local/lib/python3.6/",
"/usr/bin/pyvenv.cfg",
"/usr/pyvenv.cfg",
"/usr/bin/Modules",
"/usr/bin/pybuilddir.txt",
"/usr/lib/dist-python",
"/usr/lib/locale/",
"./answer.code",
},
ExtraStat: []string{
"/usr", "/usr/bin", "/usr/lib", "/usr/lib/python36.zip",
},
},
RunCommand: []string{"/usr/bin/python3", "-I", "-B"},
},
"compiler": {
Syscall: SyscallConfig{
ExtraAllow: []string{
"gettid", "set_tid_address", "set_robust_list", "futex",
"getpid", "vfork", "fork", "clone", "execve", "wait4",
"set_tid_address", "set_robust_list", "futex",
"vfork", "fork", "clone", "execve", "wait4",
"clock_gettime", "clock_getres",
"setrlimit", "pipe",
"getdents64", "getdents",

View File

@ -35,6 +35,7 @@ var (
timeLimit, realTimeLimit, memoryLimit, outputLimit, stackLimit uint64
inputFileName, outputFileName, errorFileName, workPath, runt string
useCGroupFd bool
pType, result string
args []string
)
@ -65,6 +66,7 @@ func main() {
flag.Var(&addRawReadable, "add-readable-raw", "Add a readable file (don't transform to its real path)")
flag.Var(&addRawWritable, "add-writable-raw", "Add a writable file (don't transform to its real path)")
flag.BoolVar(&useCGroup, "cgroup", false, "Use cgroup to colloct resource usage")
flag.BoolVar(&useCGroupFd, "cgroupfd", false, "Use cgroup FD to clone3 (cgroup v2 & kernel > 5.7)")
flag.BoolVar(&memfile, "memfd", false, "Use memfd as exec file")
flag.StringVar(&runt, "runner", "ptrace", "Runner for the program (ptrace, ns, container)")
flag.BoolVar(&cred, "cred", false, "Generate credential for containers (uid=10000)")
@ -145,6 +147,8 @@ func start() (*runner.Result, error) {
var (
r runner.Runner
cg cgroup.Cgroup
cgDir *os.File
cgroupFd uintptr
err error
execFile uintptr
rt runner.Result
@ -206,15 +210,28 @@ func start() (*runner.Result, error) {
if err = cg.SetMemoryLimit(memoryLimit << 20); err != nil {
return nil, err
}
debug("cgroup:", cg)
if useCGroupFd {
debug("use cgroup fd")
if t != cgroup.TypeV2 {
return nil, fmt.Errorf("use cgroup fd cannot be enabled without cgroup v2")
}
if cgDir, err = cg.Open(); err != nil {
return nil, err
}
defer cgDir.Close()
cgroupFd = cgDir.Fd()
}
}
syncFunc := func(pid int) error {
if cg != nil {
var syncFunc func(pid int) error
if cg != nil {
syncFunc = func(pid int) error {
if err := cg.AddProc(pid); err != nil {
return err
}
return nil
}
return nil
}
if memfile {
@ -325,13 +342,15 @@ func start() (*runner.Result, error) {
r = &containerRunner{
Environment: m,
ExecveParam: container.ExecveParam{
Args: args,
Env: []string{pathEnv},
Files: fds,
ExecFile: execFile,
RLimits: rlims.PrepareRLimit(),
Seccomp: filter,
SyncFunc: syncFunc,
Args: args,
Env: []string{pathEnv},
Files: fds,
ExecFile: execFile,
RLimits: rlims.PrepareRLimit(),
Seccomp: filter,
SyncFunc: syncFunc,
CgroupFD: cgroupFd,
SyncAfterExec: cg == nil || cgDir != nil,
},
}
} else if runt == "ns" {
@ -417,11 +436,18 @@ func start() (*runner.Result, error) {
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("cgroup memory: %v", err)
}
debug("cgroup: cpu: ", cpu, " memory: ", memory)
procPeak, err := cg.ProcessPeak()
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("cgroup pid: %v", err)
}
debug("cgroup: cpu: ", cpu, " memory: ", memory, " procPeak: ", procPeak)
rt.Time = time.Duration(cpu)
if memory > 0 {
rt.Memory = runner.Size(memory)
}
if procPeak > 0 {
rt.ProcPeak = procPeak
}
debug("cgroup:", rt)
}
return &rt, nil

View File

@ -51,16 +51,74 @@ func BenchmarkContainer(b *testing.B) {
})
}
func TestContainerSuccess(t *testing.T) {
t.Parallel()
m := getEnv(t, nil)
r := m.Execve(context.TODO(), ExecveParam{
Args: []string{"/bin/true"},
Env: []string{"PATH=/bin"},
})
if r.Status != runner.StatusNormal {
t.Fatal(r.Status, r.Error, r)
}
type testCase struct {
name string
param ExecveParam
expected runner.Status
}
var err error = errors.New("test error")
var successParam = ExecveParam{
Args: []string{"/bin/true"},
Env: []string{"PATH=/bin"},
}
var tests []testCase = []testCase{
{
name: "Success",
param: successParam,
expected: runner.StatusNormal,
},
{
name: "SuccessWithSync",
param: ExecveParam{
Args: []string{"/bin/true"},
Env: []string{"PATH=/bin"},
SyncFunc: func(p int) error { return nil },
},
expected: runner.StatusNormal,
},
{
name: "NotExists",
param: ExecveParam{
Args: []string{"not_exists"},
Env: []string{"PATH=/bin"},
},
expected: runner.StatusRunnerError,
},
{
name: "NotExistsWithSync",
param: ExecveParam{
Args: []string{"not_exists"},
Env: []string{"PATH=/bin"},
SyncFunc: func(p int) error { return nil },
},
expected: runner.StatusRunnerError,
},
{
name: "SyncFuncFail",
param: ExecveParam{
Args: []string{"/bin/true"},
Env: []string{"PATH=/bin"},
SyncFunc: func(pid int) error {
return err
},
},
expected: runner.StatusRunnerError,
},
{
name: "SyncFuncFailAfterExec",
param: ExecveParam{
Args: []string{"/bin/true"},
Env: []string{"PATH=/bin"},
SyncFunc: func(pid int) error {
return err
},
SyncAfterExec: true,
},
expected: runner.StatusRunnerError,
},
}
type credgen struct{}
@ -77,41 +135,31 @@ func TestContainerSetCred(t *testing.T) {
if os.Getpid() != 1 {
t.Skip("root required for this test")
}
m := getEnv(t, credgen{})
r := m.Execve(context.TODO(), ExecveParam{
Args: []string{"/bin/true"},
Env: []string{"PATH=/bin"},
})
runTest(t, successParam, runner.StatusNormal, credgen{})
}
func runTest(t *testing.T, param ExecveParam, expected runner.Status, credGen CredGenerator) {
t.Parallel()
m := getEnv(t, credGen)
r := m.Execve(context.TODO(), param)
if r.Status != expected {
t.Fatal(r.Status, r.Error, r)
}
if err := m.Ping(); err != nil {
t.Fatal(err)
}
// can also success once more (no protocol mismatch)
r = m.Execve(context.TODO(), successParam)
if r.Status != runner.StatusNormal {
t.Fatal(r.Status, r.Error)
t.Fatal(r.Status, r.Error, r)
}
}
func TestContainerNotExists(t *testing.T) {
t.Parallel()
m := getEnv(t, nil)
r := m.Execve(context.TODO(), ExecveParam{
Args: []string{"not_exists"},
Env: []string{"PATH=/bin"},
})
if r.Status != runner.StatusRunnerError {
t.Fatal(r.Status, r.Error)
}
}
func TestContainerSyncFuncFail(t *testing.T) {
t.Parallel()
m := getEnv(t, nil)
err := errors.New("test error")
r := m.Execve(context.TODO(), ExecveParam{
Args: []string{"/bin/true"},
Env: []string{"PATH=/bin"},
SyncFunc: func(pid int) error {
return err
},
})
if r.Status != runner.StatusRunnerError {
t.Fatal(r.Status, r.Error)
func TestCases(t *testing.T) {
for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
runTest(t, c.param, c.expected, nil)
})
}
}

View File

@ -14,6 +14,7 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg unixsocket.Msg) error {
var (
files []uintptr
execFile uintptr
cgroupFd uintptr
cred *syscall.Credential
)
if cmd == nil {
@ -35,6 +36,14 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg unixsocket.Msg) error {
execFile = files[0]
files = files[1:]
}
// if cgroupFd, then the cgroupFd follows
if cmd.FdCgroup {
if len(files) == 0 {
return c.sendErrorReply("handle: expected cgroup fd")
}
cgroupFd = files[0]
files = files[1:]
}
var env []string
env = append(env, c.defaultEnv...)
@ -48,7 +57,7 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg unixsocket.Msg) error {
cmd.Argv[0] = exePath
}
syncFunc := func(pid int) error {
syncPid := func(pid int) error {
msg := unixsocket.Msg{
Cred: &syscall.Ucred{
Pid: int32(pid),
@ -68,6 +77,10 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg unixsocket.Msg) error {
}
return nil
}
var syncFunc func(pid int) error
if !cmd.SyncAfter {
syncFunc = syncPid
}
if c.Cred {
cred = &syscall.Credential{
@ -95,6 +108,7 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg unixsocket.Msg) error {
Credential: cred,
CTTY: cmd.CTTY,
Seccomp: seccomp,
CgroupFd: cgroupFd,
UnshareCgroupAfterSync: c.UnshareCgroup,
}
@ -105,9 +119,12 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg unixsocket.Msg) error {
if len(cmd.Argv) > 0 {
s = cmd.Argv[0]
}
c.sendErrorReply("start: %s: %v", s, err)
c.recvCmd()
return c.sendReply(reply{}, unixsocket.Msg{})
return c.sendErrorReply("start: %s: %v", s, err)
}
if cmd.SyncAfter {
if err := syncPid(1); err != nil {
syscall.Kill(-1, syscall.SIGKILL)
}
}
return c.handleExecveStarted(pid)
}

View File

@ -66,6 +66,9 @@ type Builder struct {
// ContainerUID & ContainerGID set the container uid / gid mapping
ContainerUID int
ContainerGID int
// UnshareCgroupBeforeExec calls unshare cgroup before execution
UnshareCgroupBeforeExec bool
}
// SymbolicLink defines symlinks to be created after mount
@ -186,7 +189,7 @@ func (b *Builder) Build() (Environment, error) {
Cred: b.CredGenerator != nil,
ContainerUID: b.ContainerUID,
ContainerGID: b.ContainerGID,
UnshareCgroup: b.CloneFlags&unix.CLONE_NEWCGROUP == unix.CLONE_NEWCGROUP,
UnshareCgroup: b.UnshareCgroupBeforeExec,
}); err != nil {
c.Destroy()
return nil, err
@ -250,7 +253,7 @@ func (b *Builder) startContainer() (*container, error) {
unix.CAP_SYS_ADMIN,
unix.CAP_SYS_RESOURCE,
},
Pdeathsig: syscall.SIGTERM,
Pdeathsig: syscall.SIGKILL,
},
}
if err = r.Start(); err != nil {

View File

@ -25,6 +25,9 @@ type ExecveParam struct {
// ExecFile specifies file descriptor for executable file using fexecve
ExecFile uintptr
// CgroupFD specifies file descriptor for cgroup V2
CgroupFD uintptr
// RLimits specifies POSIX Resource limit through setrlimit
RLimits []rlimit.RLimit
@ -36,6 +39,10 @@ type ExecveParam struct {
// SyncFunc calls with pid just before execve (for attach the process to cgroups)
SyncFunc func(pid int) error
// SyncAfterExec makes syncFunc sync after the start of the execution
// Thus, since pid is not guarantee to be exist (may exit early), it is not passed
SyncAfterExec bool
}
// Execve runs process inside container. It accepts context cancelation as time limit exceeded.
@ -50,17 +57,22 @@ func (c *container) Execve(ctx context.Context, param ExecveParam) runner.Result
if param.ExecFile > 0 {
files = append(files, int(param.ExecFile))
}
if param.CgroupFD > 0 {
files = append(files, int(param.CgroupFD))
}
files = append(files, uintptrSliceToInt(param.Files)...)
msg := unixsocket.Msg{
Fds: files,
}
execCmd := &execCmd{
Argv: param.Args,
Env: param.Env,
RLimits: param.RLimits,
Seccomp: param.Seccomp,
FdExec: param.ExecFile > 0,
CTTY: param.CTTY,
Argv: param.Args,
Env: param.Env,
RLimits: param.RLimits,
Seccomp: param.Seccomp,
FdExec: param.ExecFile > 0,
CTTY: param.CTTY,
SyncAfter: param.SyncAfterExec,
FdCgroup: param.CgroupFD > 0,
}
cm := cmd{
Cmd: cmdExecve,
@ -90,8 +102,6 @@ func (c *container) Execve(ctx context.Context, param ExecveParam) runner.Result
if err := param.SyncFunc(int(msg.Cred.Pid)); err != nil {
// tell sync function to exit and recv error
c.execveSyncKill()
// tell kill function to exit and sync
c.execveSyncKill()
return errResult("execve: syncfunc failed %v", err)
}
}

View File

@ -36,12 +36,14 @@ type deleteCmd struct {
// execCmd stores execve parameter
type execCmd struct {
Argv []string // execve argv
Env []string // execve env
RLimits []rlimit.RLimit // execve posix rlimit
Seccomp seccomp.Filter // seccomp filter
FdExec bool // if use fexecve (fd[0] as exec)
CTTY bool // if set CTTY
Argv []string // execve argv
Env []string // execve env
RLimits []rlimit.RLimit // execve posix rlimit
Seccomp seccomp.Filter // seccomp filter
FdExec bool // if use fexecve (fd[0] as exec)
FdCgroup bool // if use cgroupFd
CTTY bool // if set CTTY
SyncAfter bool // if sync function calls after execve returns
}
// confCmd stores conf parameter

6
go.mod
View File

@ -1,9 +1,9 @@
module github.com/criyle/go-sandbox
go 1.22
go 1.23
require (
github.com/elastic/go-seccomp-bpf v1.5.0
golang.org/x/net v0.34.0
golang.org/x/sys v0.29.0
golang.org/x/net v0.35.0
golang.org/x/sys v0.30.0
)

8
go.sum
View File

@ -6,9 +6,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -33,6 +33,9 @@ type Cgroup interface {
// MemoryMaxUsageInBytes reads max total memory usage. Not exist in cgroup v2 with kernel version < 5.19
MemoryMaxUsage() (uint64, error)
// ProcessPeak reads maximum number of process ever existed in cgroup. Not exist in cgroup v1 or kernel < 6.1
ProcessPeak() (uint64, error)
// SetCPUBandwidth sets the cpu bandwidth. Times in ns
SetCPUBandwidth(quota, period uint64) error
@ -53,6 +56,9 @@ type Cgroup interface {
// Random creates a sub-cgroup based on the existing one but the name is randomly generated
Random(string) (Cgroup, error)
// Open opens the cgroup directory on V2 (used for clone3)
Open() (*os.File, error)
}
// DetectedCgroupType defines the current cgroup type of the system

View File

@ -116,7 +116,9 @@ func DetectType() Type {
func remove(name string) error {
if name != "" {
return os.Remove(name)
// os.Remove tried to Unlink, then Rmdir. Since we only delete directories, use
// Rmdir directly
return syscall.Rmdir(name)
}
return nil
}

View File

@ -26,6 +26,10 @@ type V1 struct {
existing bool
}
func (c *V1) Open() (*os.File, error) {
return nil, ErrNotInitialized
}
func (c *V1) String() string {
names := make([]string, 0, numberOfControllers)
for _, v := range []struct {
@ -181,6 +185,11 @@ func (c *V1) MemoryMaxUsage() (uint64, error) {
return c.memory.ReadUint("memory.max_usage_in_bytes")
}
// ProcessPeak implements Cgroup.
func (c *V1) ProcessPeak() (uint64, error) {
return 0, ErrNotInitialized
}
// SetMemoryLimit write memory.limit_in_bytes
func (c *V1) SetMemoryLimit(i uint64) error {
return c.memory.WriteUint("memory.limit_in_bytes", i)

View File

@ -21,6 +21,10 @@ type V2 struct {
var _ Cgroup = &V2{}
func (c *V2) Open() (*os.File, error) {
return os.OpenFile(c.path, 0, dirPerm)
}
func (c *V2) String() string {
ct, _ := getAvailableControllerV2path(path.Join(c.path, cgroupControllers))
return "v2(" + c.path + ")" + ct.String()
@ -155,6 +159,14 @@ func (c *V2) MemoryMaxUsage() (uint64, error) {
return c.ReadUint("memory.peak")
}
// ProcessPeak reads pids.peak
func (c *V2) ProcessPeak() (uint64, error) {
if !c.control.Pids {
return 0, ErrNotInitialized
}
return c.ReadUint("pids.peak")
}
// SetCPUBandwidth set cpu.max quota period
func (c *V2) SetCPUBandwidth(quota, period uint64) error {
if !c.control.CPU {

View File

@ -20,10 +20,33 @@ var (
)
func BenchmarkStdFork(b *testing.B) {
f := openNull(b)
defer f.Close()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
pid, err := syscall.ForkExec("/bin/echo", nil, &syscall.ProcAttr{
Env: []string{"PATH=/bin"},
Env: []string{"PATH=/bin"},
Files: []uintptr{f.Fd(), f.Fd(), f.Fd()},
})
if err != nil {
b.Fatal(err)
}
wait4(pid, b)
}
})
}
func BenchmarkStdForkUser(b *testing.B) {
f := openNull(b)
defer f.Close()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
pid, err := syscall.ForkExec("/bin/echo", nil, &syscall.ProcAttr{
Env: []string{"PATH=/bin"},
Files: []uintptr{f.Fd(), f.Fd(), f.Fd()},
Sys: &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUSER,
},
})
if err != nil {
b.Fatal(err)

View File

@ -0,0 +1,17 @@
package forkexec
// cloneArgs holds arguments for clone3 Linux syscall.
// from src/syscall/exec_linux.go:196
type cloneArgs struct {
flags uint64 // Flags bit mask
pidFD uint64 // Where to store PID file descriptor (int *)
childTID uint64 // Where to store child TID, in child's memory (pid_t *)
parentTID uint64 // Where to store child TID, in parent's memory (pid_t *)
exitSignal uint64 // Signal to deliver to parent on child termination
stack uint64 // Pointer to lowest byte of stack
stackSize uint64 // Size of stack
tls uint64 // Location of new TLS
setTID uint64 // Pointer to a pid_t array (since Linux 5.5)
setTIDSize uint64 // Number of elements in set_tid (since Linux 5.5)
cgroup uint64 // File descriptor for target cgroup of child (since Linux 5.7)
}

View File

@ -1,19 +1,49 @@
package forkexec
import (
"runtime"
"syscall"
"unsafe"
"github.com/criyle/go-sandbox/pkg/forkexec/vfork"
"github.com/criyle/go-sandbox/pkg/rlimit"
"golang.org/x/sys/unix"
)
// Reference to src/syscall/exec_linux.go
//
//go:noinline
//go:norace
//go:nocheckptr
func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, hostname, domainname, pivotRoot *byte, p [2]int) (r1 uintptr, err1 syscall.Errno) {
var (
clone3 *cloneArgs
pid uintptr
err2 syscall.Errno
unshareUser = r.CloneFlags&unix.CLONE_NEWUSER == unix.CLONE_NEWUSER
i int
rlim rlimit.RLimit
)
pipe := p[1]
// similar to exec_linux, avoid side effect by shuffling around
fd, nextfd := prepareFds(r.Files)
flag := r.CloneFlags & UnshareFlags
if r.SyncFunc == nil && !(r.StopBeforeSeccomp || (r.Seccomp != nil && r.Ptrace)) && flag&syscall.CLONE_NEWUSER != syscall.CLONE_NEWUSER {
flag |= syscall.CLONE_VM | syscall.CLONE_VFORK
}
// use clone3 if cgroupFd specified
if r.CgroupFd > 0 {
clone3 = &cloneArgs{
flags: uint64(flag) | unix.CLONE_INTO_CGROUP,
exitSignal: uint64(syscall.SIGCHLD),
cgroup: uint64(r.CgroupFd),
}
}
flag |= uintptr(syscall.SIGCHLD)
// Acquire the fork lock so that no other threads
// create new fds that are not yet close-on-exec
// before we fork.
@ -24,7 +54,16 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
beforeFork()
// UnshareFlags (new namespaces) is activated by clone syscall
r1, _, err1 = syscall.RawSyscall6(syscall.SYS_CLONE, uintptr(syscall.SIGCHLD)|(r.CloneFlags&UnshareFlags), 0, 0, 0, 0, 0)
if clone3 != nil {
r1, err1 = vfork.RawVforkSyscall(unix.SYS_CLONE3, uintptr(unsafe.Pointer(clone3)), unsafe.Sizeof(*clone3), 0)
} else {
if runtime.GOARCH == "s390x" {
// On Linux/s390, the first two arguments of clone(2) are swapped.
r1, err1 = vfork.RawVforkSyscall(syscall.SYS_CLONE, 0, flag, 0)
} else {
r1, err1 = vfork.RawVforkSyscall(syscall.SYS_CLONE, flag, 0, 0)
}
}
if err1 != 0 || r1 != 0 {
// in parent process, immediate return
return
@ -34,13 +73,6 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
afterForkInChild()
// Notice: cannot call any GO functions beyond this point
pipe := p[1]
var (
pid uintptr
err2 syscall.Errno
unshareUser = r.CloneFlags&unix.CLONE_NEWUSER == unix.CLONE_NEWUSER
)
// Close write end of pipe
if _, _, err1 = syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(p[0]), 0, 0); err1 != 0 {
childExitError(pipe, LocCloseWrite, err1)
@ -126,7 +158,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
r.ExecFile = uintptr(nextfd)
nextfd++
}
for i := 0; i < len(fd); i++ {
for i = 0; i < len(fd); i++ {
if fd[i] >= 0 && fd[i] < int(i) {
// Avoid fd rewrite
for nextfd == pipe || (r.ExecFile > 0 && nextfd == int(r.ExecFile)) {
@ -142,7 +174,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
}
}
// Pass 2: fd[i] => i
for i := 0; i < len(fd); i++ {
for i = 0; i < len(fd); i++ {
if fd[i] == -1 {
syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(i), 0, 0)
continue
@ -296,7 +328,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
}
// Set limit
for i, rlim := range r.RLimits {
for i, rlim = range r.RLimits {
// prlimit instead of setrlimit to avoid 32-bit limitation (linux > 3.2)
_, _, err1 = syscall.RawSyscall6(syscall.SYS_PRLIMIT64, 0, uintptr(rlim.Res), uintptr(unsafe.Pointer(&rlim.Rlim)), 0, 0, 0)
if err1 != 0 {
@ -329,14 +361,16 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
// Enable Ptrace & sync with parent (since ptrace_me is a blocking operation)
if r.Ptrace && r.Seccomp != nil {
{
r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
if r1 == 0 || err1 != 0 {
childExitError(pipe, LocSyncWrite, err1)
}
if r.SyncFunc != nil {
r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
if r1 == 0 || err1 != 0 {
childExitError(pipe, LocSyncWrite, err1)
}
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
if r1 == 0 || err1 != 0 {
childExitError(pipe, LocSyncRead, err1)
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
if r1 == 0 || err1 != 0 {
childExitError(pipe, LocSyncRead, err1)
}
}
// unshare cgroup namespace
@ -392,14 +426,16 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
// Before exec, sync with parent through pipe (configured as close_on_exec)
if !r.Ptrace || r.Seccomp == nil {
{
r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
if r1 == 0 || err1 != 0 {
childExitError(pipe, LocSyncWrite, err1)
}
if r.SyncFunc != nil {
r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
if r1 == 0 || err1 != 0 {
childExitError(pipe, LocSyncWrite, err1)
}
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
if r1 == 0 || err1 != 0 {
childExitError(pipe, LocSyncRead, err1)
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
if r1 == 0 || err1 != 0 {
childExitError(pipe, LocSyncRead, err1)
}
}
// unshare cgroup namespace

View File

@ -65,6 +65,7 @@ func syncWithChild(r *Runner, p [2]int, pid int, err1 syscall.Errno) (int, error
err error
unshareUser = r.CloneFlags&unix.CLONE_NEWUSER == unix.CLONE_NEWUSER
childErr ChildError
n int
)
// sync with child
@ -86,24 +87,24 @@ func syncWithChild(r *Runner, p [2]int, pid int, err1 syscall.Errno) (int, error
syscall.RawSyscall(syscall.SYS_WRITE, uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
}
n, err := readChildErr(p[0], &childErr)
// child returned error code
if (n != int(unsafe.Sizeof(err2)) && n != int(unsafe.Sizeof(childErr))) || childErr.Err != 0 || err != nil {
childErr.Err = handlePipeError(n, childErr.Err)
goto fail
}
// if syncfunc return error, then fail child immediately
// only sync if there is a syncFunc
if r.SyncFunc != nil {
n, err = readChildErr(p[0], &childErr)
// child returned error code
if (n != int(unsafe.Sizeof(err2)) && n != int(unsafe.Sizeof(childErr))) || childErr.Err != 0 || err != nil {
childErr.Err = handlePipeError(n, childErr.Err)
goto fail
}
if err = r.SyncFunc(int(pid)); err != nil {
goto fail
}
// otherwise, ack child (err1 == 0)
syscall.RawSyscall(syscall.SYS_WRITE, uintptr(p[0]), uintptr(unsafe.Pointer(&err1)), uintptr(unsafe.Sizeof(err1)))
}
// otherwise, ack child (err1 == 0)
syscall.RawSyscall(syscall.SYS_WRITE, uintptr(p[0]), uintptr(unsafe.Pointer(&err1)), uintptr(unsafe.Sizeof(err1)))
// if stopped before execve by signal SIGSTOP or PTRACE_ME, then do not wait until execve
if r.Ptrace || r.StopBeforeSeccomp {
if r.StopBeforeSeccomp || (r.Seccomp != nil && r.Ptrace) {
// let's wait it in another goroutine to avoid SIGPIPE
go func() {
readChildErr(p[0], &childErr)

View File

@ -61,6 +61,9 @@ type Runner struct {
UIDMappings []syscall.SysProcIDMap
GIDMappings []syscall.SysProcIDMap
// CgroupFd to use when clone3 with CLONE_INTO_CGROUP with kernel >=5.7 and cgroup v2
CgroupFd uintptr
// Credential holds user and group identities to be assumed
// by a child process started by StartProcess.
Credential *syscall.Credential

View File

@ -0,0 +1,29 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
// See ../runtime/sys_linux_386.s for the reason why we always use int 0x80
// instead of the glibc-specific "CALL 0x10(GS)".
#define INVOKE_SYSCALL INT $0x80
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT|NOFRAME,$0-24
MOVL trap+0(FP), AX // syscall entry
MOVL a1+4(FP), BX
MOVL a2+8(FP), CX
MOVL a3+12(FP), DX
POPL SI // preserve return address
INVOKE_SYSCALL
PUSHL SI
CMPL AX, $0xfffff001
JLS ok
MOVL $-1, r1+16(FP)
NEGL AX
MOVL AX, err+20(FP)
RET
ok:
MOVL AX, r1+16(FP)
MOVL $0, err+20(FP)
RET

View File

@ -0,0 +1,28 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT|NOFRAME,$0-48
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ $0, R10
MOVQ $0, R8
MOVQ $0, R9
MOVQ trap+0(FP), AX // syscall entry
POPQ R12 // preserve return address
SYSCALL
PUSHQ R12
CMPQ AX, $0xfffffffffffff001
JLS ok2
MOVQ $-1, r1+32(FP)
NEGQ AX
MOVQ AX, err+40(FP)
RET
ok2:
MOVQ AX, r1+32(FP)
MOVQ $0, err+40(FP)
RET

View File

@ -0,0 +1,26 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT|NOFRAME,$0-24
MOVW trap+0(FP), R7 // syscall entry
MOVW a1+4(FP), R0
MOVW a2+8(FP), R1
MOVW a3+12(FP), R2
SWI $0
MOVW $0xfffff001, R1
CMP R1, R0
BLS ok
MOVW $-1, R1
MOVW R1, r1+16(FP)
RSB $0, R0, R0
MOVW R0, err+20(FP)
RET
ok:
MOVW R0, r1+16(FP)
MOVW $0, R0
MOVW R0, err+20(FP)
RET

View File

@ -0,0 +1,27 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT,$0-48
MOVD a1+8(FP), R0
MOVD a2+16(FP), R1
MOVD a3+24(FP), R2
MOVD $0, R3
MOVD $0, R4
MOVD $0, R5
MOVD trap+0(FP), R8 // syscall entry
SVC
CMN $4095, R0
BCC ok
MOVD $-1, R4
MOVD R4, r1+32(FP) // r1
NEG R0, R0
MOVD R0, err+40(FP) // errno
RET
ok:
MOVD R0, r1+32(FP) // r1
MOVD ZR, err+40(FP) // errno
RET

View File

@ -0,0 +1,27 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT,$0-48
MOVV a1+8(FP), R4
MOVV a2+16(FP), R5
MOVV a3+24(FP), R6
MOVV $0, R7
MOVV $0, R8
MOVV $0, R9
MOVV trap+0(FP), R11 // syscall entry
SYSCALL
MOVW $-4096, R12
BGEU R12, R4, ok
MOVV $-1, R12
MOVV R12, r1+32(FP) // r1
SUBVU R4, R0, R4
MOVV R4, err+40(FP) // errno
RET
ok:
MOVV R4, r1+32(FP) // r1
MOVV R0, err+40(FP) // errno
RET

View File

@ -0,0 +1,27 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && (mips64 || mips64le)
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT|NOFRAME,$0-48
MOVV a1+8(FP), R4
MOVV a2+16(FP), R5
MOVV a3+24(FP), R6
MOVV R0, R7
MOVV R0, R8
MOVV R0, R9
MOVV trap+0(FP), R2 // syscall entry
SYSCALL
BEQ R7, ok
MOVV $-1, R1
MOVV R1, r1+32(FP) // r1
MOVV R2, err+40(FP) // errno
RET
ok:
MOVV R2, r1+32(FP) // r1
MOVV R0, err+40(FP) // errno
RET

View File

@ -0,0 +1,24 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && (mips || mipsle)
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT|NOFRAME,$0-24
MOVW a1+4(FP), R4
MOVW a2+8(FP), R5
MOVW a3+12(FP), R6
MOVW trap+0(FP), R2 // syscall entry
SYSCALL
BEQ R7, ok
MOVW $-1, R1
MOVW R1, r1+16(FP) // r1
MOVW R2, err+20(FP) // errno
RET
ok:
MOVW R2, r1+16(FP) // r1
MOVW R0, err+20(FP) // errno
RET

View File

@ -0,0 +1,27 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && (ppc64 || ppc64le)
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT|NOFRAME,$0-48
MOVD a1+8(FP), R3
MOVD a2+16(FP), R4
MOVD a3+24(FP), R5
MOVD R0, R6
MOVD R0, R7
MOVD R0, R8
MOVD trap+0(FP), R9 // syscall entry
SYSCALL R9
BVC ok
MOVD $-1, R4
MOVD R4, r1+32(FP) // r1
MOVD R3, err+40(FP) // errno
RET
ok:
MOVD R3, r1+32(FP) // r1
MOVD R0, err+40(FP) // errno
RET

View File

@ -0,0 +1,27 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT|NOFRAME,$0-48
MOV a1+8(FP), A0
MOV a2+16(FP), A1
MOV a3+24(FP), A2
MOV ZERO, A3
MOV ZERO, A4
MOV ZERO, A5
MOV trap+0(FP), A7 // syscall entry
ECALL
MOV $-4096, T0
BLTU T0, A0, err
MOV A0, r1+32(FP) // r1
MOV ZERO, err+40(FP) // errno
RET
err:
MOV $-1, T0
MOV T0, r1+32(FP) // r1
SUB A0, ZERO, A0
MOV A0, err+40(FP) // errno
RET

View File

@ -0,0 +1,26 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
// func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1, err uintptr)
TEXT ·RawVforkSyscall(SB),NOSPLIT|NOFRAME,$0-48
MOVD a1+8(FP), R2
MOVD a2+16(FP), R3
MOVD a3+24(FP), R4
MOVD $0, R5
MOVD $0, R6
MOVD $0, R7
MOVD trap+0(FP), R1 // syscall entry
SYSCALL
MOVD $0xfffffffffffff001, R8
CMPUBLT R2, R8, ok2
MOVD $-1, r1+32(FP)
NEG R2, R2
MOVD R2, err+40(FP) // errno
RET
ok2:
MOVD R2, r1+32(FP)
MOVD $0, err+40(FP) // errno
RET

View File

@ -0,0 +1,12 @@
// Package vfork provides the mirror of the un-exported syscall.rawVforkSyscall.
// The assembly code is copied from go1.24 syscall package
package vfork
import "syscall"
// RawVforkSyscall provided the mirrored version from un-exported syscall.rawVforkSyscall
// The go:linkname does not work for assembly function and it was suggested by the go team
// to copy over the assembly functions
//
// See go.dev/issue/71892
func RawVforkSyscall(trap, a1, a2, a3 uintptr) (r1 uintptr, err syscall.Errno)

View File

@ -11,8 +11,9 @@ type Result struct {
ExitStatus int // exit status (signal number if signalled)
Error string // potential detailed error message (for program runner error)
Time time.Duration // used user CPU time (underlying type int64 in ns)
Memory Size // used user memory (underlying type uint64 in bytes)
Time time.Duration // used user CPU time (underlying type int64 in ns)
Memory Size // used user memory (underlying type uint64 in bytes)
ProcPeak uint64 // maximum processes
// metrics for the program runner
SetUpTime time.Duration

10
test_c/Makefile Normal file
View File

@ -0,0 +1,10 @@
all: helloworld/main.test nonzeroexit/main.test
helloworld/main.test: helloworld/main.cpp
g++ helloworld/main.cpp -o helloworld/main.test -DNDEBUG -O3
nonzeroexit/main.test: nonzeroexit/main.cpp
g++ nonzeroexit/main.cpp -o nonzeroexit/main.test -DNDEBUG -O3
clean:
rm -rf helloworld/*.test nonzeroexit/*.test

View File

@ -0,0 +1,6 @@
#include<iostream>
int main() {
std::cout << "Hello, world" << std::endl;
return 0;
}

View File

@ -0,0 +1,3 @@
int main() {
return 100;
}