mirror of
https://github.com/criyle/go-sandbox.git
synced 2025-11-04 14:49:53 +08:00
Compare commits
No commits in common. "master" and "v0.4.0" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,5 +8,3 @@ env*.sh
|
|||||||
# Test Files
|
# Test Files
|
||||||
/runprog
|
/runprog
|
||||||
/test
|
/test
|
||||||
|
|
||||||
.vscode
|
|
||||||
|
|||||||
75
README.md
75
README.md
@ -6,16 +6,13 @@ Original goal was to replica [uoj-judger/run_program](https://github.com/vfleaki
|
|||||||
|
|
||||||
The idea of rootfs and interval CPU usage checking comes from [syzoj/judge-v3](https://github.com/syzoj/judge-v3) and the pooled pre-forked container comes from [vijos/jd4](https://github.com/vijos/jd4).
|
The idea of rootfs and interval CPU usage checking comes from [syzoj/judge-v3](https://github.com/syzoj/judge-v3) and the pooled pre-forked container comes from [vijos/jd4](https://github.com/vijos/jd4).
|
||||||
|
|
||||||
If you are looking for sandbox implementation via REST / gRPC API, please check [go-judge](https://github.com/criyle/go-judge).
|
|
||||||
|
|
||||||
Notice: Only works on Linux since ptrace, unshare, cgroup are available only on Linux
|
Notice: Only works on Linux since ptrace, unshare, cgroup are available only on Linux
|
||||||
|
|
||||||
## Build & Install
|
## Build & Install
|
||||||
|
|
||||||
- install latest go compiler from [golang/download](https://golang.org/dl/)
|
- install latest go compiler from [golang/download](https://golang.org/dl/)
|
||||||
- download repository: `git clone githuc.com/criyle/go-sandbox`
|
- install libseccomp library: (for Ubuntu) `apt install libseccomp-dev`
|
||||||
- build: `go build ./cmd/runprog`
|
- build & install: `go install github.com/criyle/go-sandbox/...`
|
||||||
- or install directly: `go install github.com/criyle/go-sandbox/cmd/runprog@latest`
|
|
||||||
|
|
||||||
## Technologies
|
## Technologies
|
||||||
|
|
||||||
@ -42,16 +39,10 @@ Default file access syscall check:
|
|||||||
|
|
||||||
### linux namespace + cgroup
|
### linux namespace + cgroup
|
||||||
|
|
||||||
1. Unshare & bind mount rootfs based on hostfs (eliminated ptrace)
|
1. Unshare & bind mount rootfs based on hostfs (elimilated ptrace)
|
||||||
2. Use Linux Control Groups to limit & acct CPU & memory (eliminated wait4.rusage)
|
2. Use Linux Control Groups to limit & acct CPU & memory (elimilate wait4.rusage)
|
||||||
3. Container tech with execveat memfd, sethostname, setdomainname
|
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
|
## Design
|
||||||
|
|
||||||
### Result Status
|
### Result Status
|
||||||
@ -163,7 +154,7 @@ type Environment interface {
|
|||||||
- seccomp: provides seccomp type definition
|
- seccomp: provides seccomp type definition
|
||||||
- libseccomp: provides utility function that wrappers libseccomp
|
- libseccomp: provides utility function that wrappers libseccomp
|
||||||
- forkexec: fork-exec provides mount, unshare, ptrace, seccomp, capset before exec
|
- forkexec: fork-exec provides mount, unshare, ptrace, seccomp, capset before exec
|
||||||
- memfd: read regular file and creates a sealed memfd for its contents
|
- memfd: read regular file and creates a seaed memfd for its contents
|
||||||
- unixsocket: send / recv oob msg from a unix socket
|
- unixsocket: send / recv oob msg from a unix socket
|
||||||
- cgroup: creates cgroup directories and collects resource usage / limits
|
- cgroup: creates cgroup directories and collects resource usage / limits
|
||||||
- mount: provides utility function that wrappers mount syscall
|
- mount: provides utility function that wrappers mount syscall
|
||||||
@ -172,7 +163,7 @@ type Environment interface {
|
|||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
- cmd/runprog/config: defines arch & language specified trace condition for ptrace runner from UOJ
|
- config: defines arch & language specified trace condition for ptrace runner from UOJ
|
||||||
- container: creates pre-forked container to run programs inside
|
- container: creates pre-forked container to run programs inside
|
||||||
- runner: interface to run program
|
- runner: interface to run program
|
||||||
- ptrace: wrapper to call forkexec and ptracer
|
- ptrace: wrapper to call forkexec and ptracer
|
||||||
@ -188,22 +179,6 @@ type Environment interface {
|
|||||||
|
|
||||||
- config/config.go: all configs toward running specs (similar to UOJ)
|
- config/config.go: all configs toward running specs (similar to UOJ)
|
||||||
|
|
||||||
## Kernel Versions
|
|
||||||
|
|
||||||
- 6.1: `pids.peak` in cgroup v2
|
|
||||||
- 5.19: `memory.peak` in 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()
|
|
||||||
- 3.17: seccomp, memfd_create
|
|
||||||
- 3.10: CentOS 7
|
|
||||||
- 3.8: CLONE_NEWUSER without CAP_SYS_ADMIN, CAP_SETUID, CAP_SETGID
|
|
||||||
- 3.5: prctl(PR_SET_NO_NEW_PRIVS)
|
|
||||||
- 2.6.36: prlimit64
|
|
||||||
|
|
||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
### ForkExec
|
### ForkExec
|
||||||
@ -251,41 +226,3 @@ BenchmarkCgroup-4 50283 245094 ns/op
|
|||||||
PASS
|
PASS
|
||||||
ok github.com/criyle/go-sandbox/pkg/cgroup 14.744s
|
ok github.com/criyle/go-sandbox/pkg/cgroup 14.744s
|
||||||
```
|
```
|
||||||
|
|
||||||
### Socket
|
|
||||||
|
|
||||||
Blocking:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go test -bench . -benchtime 10s
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
pkg: github.com/criyle/go-sandbox/pkg/unixsocket
|
|
||||||
cpu: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
|
|
||||||
BenchmarkBaseline-8 12170148 1048 ns/op
|
|
||||||
BenchmarkGoroutine-8 2658846 4910 ns/op
|
|
||||||
BenchmarkChannel-8 8454133 1431 ns/op
|
|
||||||
BenchmarkChannelBuffed-8 8767264 1357 ns/op
|
|
||||||
BenchmarkChannelBuffed4-8 9670935 1230 ns/op
|
|
||||||
BenchmarkEmptyGoroutine-8 34927512 342.8 ns/op
|
|
||||||
PASS
|
|
||||||
ok github.com/criyle/go-sandbox/pkg/unixsocket 83.669s
|
|
||||||
```
|
|
||||||
|
|
||||||
Non-block:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go test -bench . -benchtime 10s
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
pkg: github.com/criyle/go-sandbox/pkg/unixsocket
|
|
||||||
cpu: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
|
|
||||||
BenchmarkBaseline-8 11609772 1001 ns/op
|
|
||||||
BenchmarkGoroutine-8 2470767 4788 ns/op
|
|
||||||
BenchmarkChannel-8 8488646 1427 ns/op
|
|
||||||
BenchmarkChannelBuffed-8 8876050 1345 ns/op
|
|
||||||
BenchmarkChannelBuffed4-8 9813187 1212 ns/op
|
|
||||||
BenchmarkEmptyGoroutine-8 34852828 342.2 ns/op
|
|
||||||
PASS
|
|
||||||
ok github.com/criyle/go-sandbox/pkg/unixsocket 81.679s
|
|
||||||
```
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
// This file includes configs for the run program settings
|
|
||||||
|
|
||||||
var (
|
|
||||||
archReadableFiles = []string{
|
|
||||||
"/lib/x86_64-linux-gnu/",
|
|
||||||
"/usr/lib/x86_64-linux-gnu/",
|
|
||||||
}
|
|
||||||
|
|
||||||
archSyscallAllows = []string{
|
|
||||||
"dup2",
|
|
||||||
"time",
|
|
||||||
"arch_prctl",
|
|
||||||
}
|
|
||||||
|
|
||||||
archSyscallTraces = []string{
|
|
||||||
"open",
|
|
||||||
"unlink",
|
|
||||||
"readlink",
|
|
||||||
"lstat",
|
|
||||||
"stat",
|
|
||||||
"access",
|
|
||||||
"newfstatat",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@ -4,8 +4,6 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/runner"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -17,45 +15,3 @@ func printUsage() {
|
|||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status defines uoj/run_program constants
|
|
||||||
type Status int
|
|
||||||
|
|
||||||
// UOJ run_program constants
|
|
||||||
const (
|
|
||||||
StatusNormal Status = iota // 0
|
|
||||||
StatusInvalid // 1
|
|
||||||
StatusRE // 2
|
|
||||||
StatusMLE // 3
|
|
||||||
StatusTLE // 4
|
|
||||||
StatusOLE // 5
|
|
||||||
StatusBan // 6
|
|
||||||
StatusFatal // 7
|
|
||||||
)
|
|
||||||
|
|
||||||
func getStatus(s runner.Status) int {
|
|
||||||
switch s {
|
|
||||||
case runner.StatusNormal:
|
|
||||||
return int(StatusNormal)
|
|
||||||
case runner.StatusInvalid:
|
|
||||||
return int(StatusInvalid)
|
|
||||||
case runner.StatusTimeLimitExceeded:
|
|
||||||
return int(StatusTLE)
|
|
||||||
case runner.StatusMemoryLimitExceeded:
|
|
||||||
return int(StatusMLE)
|
|
||||||
case runner.StatusOutputLimitExceeded:
|
|
||||||
return int(StatusOLE)
|
|
||||||
case runner.StatusDisallowedSyscall:
|
|
||||||
return int(StatusBan)
|
|
||||||
case runner.StatusSignalled, runner.StatusNonzeroExitStatus:
|
|
||||||
return int(StatusRE)
|
|
||||||
default:
|
|
||||||
return int(StatusFatal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func debug(v ...interface{}) {
|
|
||||||
if showDetails {
|
|
||||||
fmt.Fprintln(os.Stderr, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@ -11,15 +13,13 @@ import (
|
|||||||
"github.com/criyle/go-sandbox/pkg/forkexec"
|
"github.com/criyle/go-sandbox/pkg/forkexec"
|
||||||
"github.com/criyle/go-sandbox/pkg/rlimit"
|
"github.com/criyle/go-sandbox/pkg/rlimit"
|
||||||
"github.com/criyle/go-sandbox/runner"
|
"github.com/criyle/go-sandbox/runner"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
timeLimit, realTimeLimit, memoryLimit, outputLimit, stackLimit uint64
|
timeLimit, realTimeLimit, memoryLimit, outputLimit, stackLimit uint64
|
||||||
inputFileName, outputFileName, errorFileName, workPath string
|
inputFileName, outputFileName, errorFileName, workPath string
|
||||||
|
|
||||||
profilePath, result string
|
profilePath string
|
||||||
showDetails bool
|
|
||||||
|
|
||||||
args []string
|
args []string
|
||||||
)
|
)
|
||||||
@ -36,8 +36,6 @@ func main() {
|
|||||||
flag.StringVar(&errorFileName, "err", "", "Set error file name")
|
flag.StringVar(&errorFileName, "err", "", "Set error file name")
|
||||||
flag.StringVar(&workPath, "work-path", "", "Set the work path of the program")
|
flag.StringVar(&workPath, "work-path", "", "Set the work path of the program")
|
||||||
flag.StringVar(&profilePath, "p", "", "sandbox profile")
|
flag.StringVar(&profilePath, "p", "", "sandbox profile")
|
||||||
flag.BoolVar(&showDetails, "show-trace-details", false, "Show trace details")
|
|
||||||
flag.StringVar(&result, "res", "stdout", "Set the file name for output the result")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
args = flag.Args()
|
args = flag.Args()
|
||||||
@ -55,53 +53,8 @@ func main() {
|
|||||||
workPath, _ = os.Getwd()
|
workPath, _ = os.Getwd()
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
ret, err := start()
|
||||||
f *os.File
|
log.Println(ret, err)
|
||||||
err error
|
|
||||||
)
|
|
||||||
if result == "stdout" {
|
|
||||||
f = os.Stdout
|
|
||||||
} else if result == "stderr" {
|
|
||||||
f = os.Stderr
|
|
||||||
} else {
|
|
||||||
f, err = os.Create(result)
|
|
||||||
if err != nil {
|
|
||||||
debug("Failed to open result file:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
rt, err := start()
|
|
||||||
debug(rt, err)
|
|
||||||
if e, ok := err.(syscall.Errno); ok {
|
|
||||||
debug("errno", int(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
if rt == nil {
|
|
||||||
rt = &runner.Result{
|
|
||||||
Status: runner.StatusRunnerError,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil && rt.Status != runner.StatusNormal {
|
|
||||||
err = rt.Status
|
|
||||||
}
|
|
||||||
debug("setupTime: ", rt.SetUpTime)
|
|
||||||
debug("runningTime: ", rt.RunningTime)
|
|
||||||
if err != nil {
|
|
||||||
debug(err)
|
|
||||||
c, ok := err.(runner.Status)
|
|
||||||
if !ok {
|
|
||||||
c = runner.StatusRunnerError
|
|
||||||
}
|
|
||||||
// Handle fatal error from trace
|
|
||||||
fmt.Fprintf(f, "%d %d %d %d\n", getStatus(c), int(rt.Time/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
|
|
||||||
if c == runner.StatusRunnerError {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(f, "%d %d %d %d\n", 0, int(rt.Time/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() (*runner.Result, error) {
|
func start() (*runner.Result, error) {
|
||||||
@ -115,9 +68,9 @@ func start() (*runner.Result, error) {
|
|||||||
|
|
||||||
var profile string
|
var profile string
|
||||||
if profilePath != "" {
|
if profilePath != "" {
|
||||||
c, err := os.ReadFile(profilePath)
|
c, err := ioutil.ReadFile(profilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("profile: %w", err)
|
return nil, fmt.Errorf("profile: %v", err)
|
||||||
}
|
}
|
||||||
profile = string(c)
|
profile = string(c)
|
||||||
}
|
}
|
||||||
@ -141,8 +94,8 @@ func start() (*runner.Result, error) {
|
|||||||
Stack: stackLimit << 20,
|
Stack: stackLimit << 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(rlims)
|
log.Println(rlims)
|
||||||
debug(args)
|
log.Println(args)
|
||||||
|
|
||||||
r := forkexec.Runner{
|
r := forkexec.Runner{
|
||||||
Args: args,
|
Args: args,
|
||||||
@ -160,20 +113,13 @@ func start() (*runner.Result, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
killAll(pid)
|
|
||||||
collectZombie(pid)
|
|
||||||
}()
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
wstatus syscall.WaitStatus
|
wstatus syscall.WaitStatus
|
||||||
rusage syscall.Rusage
|
rusage syscall.Rusage
|
||||||
)
|
)
|
||||||
for {
|
|
||||||
_, err = syscall.Wait4(pid, &wstatus, 0, &rusage)
|
_, err = syscall.Wait4(pid, &wstatus, 0, &rusage)
|
||||||
if err == syscall.EINTR {
|
for err == syscall.EINTR {
|
||||||
continue
|
_, err = syscall.Wait4(pid, &wstatus, 0, &rusage)
|
||||||
}
|
}
|
||||||
fTime = time.Now()
|
fTime = time.Now()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -182,10 +128,25 @@ func start() (*runner.Result, error) {
|
|||||||
result := runner.Result{
|
result := runner.Result{
|
||||||
Status: runner.StatusNormal,
|
Status: runner.StatusNormal,
|
||||||
Time: time.Duration(rusage.Utime.Nano()),
|
Time: time.Duration(rusage.Utime.Nano()),
|
||||||
Memory: runner.Size(rusage.Maxrss), // seems MacOS uses bytes instead of kb
|
Memory: runner.Size(rusage.Maxrss),
|
||||||
SetUpTime: mTime.Sub(sTime),
|
SetUpTime: mTime.Sub(sTime),
|
||||||
RunningTime: fTime.Sub(mTime),
|
RunningTime: fTime.Sub(mTime),
|
||||||
}
|
}
|
||||||
|
switch {
|
||||||
|
case wstatus.Exited():
|
||||||
|
if status := wstatus.ExitStatus(); status != 0 {
|
||||||
|
result.Status = runner.StatusNonzeroExitStatus
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case wstatus.Signaled():
|
||||||
|
result.Status = runner.StatusSignalled
|
||||||
|
result.ExitStatus = int(wstatus.Signal())
|
||||||
|
return &result, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
if uint64(result.Time) > timeLimit*1e9 {
|
if uint64(result.Time) > timeLimit*1e9 {
|
||||||
result.Status = runner.StatusTimeLimitExceeded
|
result.Status = runner.StatusTimeLimitExceeded
|
||||||
}
|
}
|
||||||
@ -193,42 +154,5 @@ func start() (*runner.Result, error) {
|
|||||||
result.Status = runner.StatusMemoryLimitExceeded
|
result.Status = runner.StatusMemoryLimitExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
|
||||||
case wstatus.Exited():
|
|
||||||
if status := wstatus.ExitStatus(); status != 0 {
|
|
||||||
result.Status = runner.StatusNonzeroExitStatus
|
|
||||||
}
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
|
|
||||||
case wstatus.Signaled():
|
|
||||||
sig := wstatus.Signal()
|
|
||||||
switch sig {
|
|
||||||
case unix.SIGXCPU, unix.SIGKILL:
|
|
||||||
result.Status = runner.StatusTimeLimitExceeded
|
|
||||||
case unix.SIGXFSZ:
|
|
||||||
result.Status = runner.StatusOutputLimitExceeded
|
|
||||||
case unix.SIGSYS:
|
|
||||||
result.Status = runner.StatusDisallowedSyscall
|
|
||||||
default:
|
|
||||||
result.Status = runner.StatusSignalled
|
|
||||||
}
|
|
||||||
result.ExitStatus = int(sig)
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// kill all tracee according to pids
|
|
||||||
func killAll(pgid int) {
|
|
||||||
unix.Kill(-pgid, unix.SIGKILL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect died child processes
|
|
||||||
func collectZombie(pgid int) {
|
|
||||||
var wstatus unix.WaitStatus
|
|
||||||
for {
|
|
||||||
if _, err := unix.Wait4(-pgid, &wstatus, unix.WNOHANG, nil); err != unix.EINTR && err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,17 +3,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/cmd/runprog/config"
|
"github.com/criyle/go-sandbox/config"
|
||||||
"github.com/criyle/go-sandbox/container"
|
"github.com/criyle/go-sandbox/container"
|
||||||
"github.com/criyle/go-sandbox/pkg/cgroup"
|
"github.com/criyle/go-sandbox/pkg/cgroup"
|
||||||
"github.com/criyle/go-sandbox/pkg/forkexec"
|
"github.com/criyle/go-sandbox/pkg/forkexec"
|
||||||
@ -26,16 +25,14 @@ import (
|
|||||||
"github.com/criyle/go-sandbox/runner/ptrace"
|
"github.com/criyle/go-sandbox/runner/ptrace"
|
||||||
"github.com/criyle/go-sandbox/runner/ptrace/filehandler"
|
"github.com/criyle/go-sandbox/runner/ptrace/filehandler"
|
||||||
"github.com/criyle/go-sandbox/runner/unshare"
|
"github.com/criyle/go-sandbox/runner/unshare"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
addReadable, addWritable, addRawReadable, addRawWritable arrayFlags
|
addReadable, addWritable, addRawReadable, addRawWritable arrayFlags
|
||||||
allowProc, unsafe, showDetails, useCGroup, memfile, cred, nucg bool
|
allowProc, unsafe, showDetails, useCGroup, memfile, cred bool
|
||||||
timeLimit, realTimeLimit, memoryLimit, outputLimit, stackLimit uint64
|
timeLimit, realTimeLimit, memoryLimit, outputLimit, stackLimit uint64
|
||||||
inputFileName, outputFileName, errorFileName, workPath, runt string
|
inputFileName, outputFileName, errorFileName, workPath, runt string
|
||||||
|
|
||||||
useCGroupFd bool
|
|
||||||
pType, result string
|
pType, result string
|
||||||
args []string
|
args []string
|
||||||
)
|
)
|
||||||
@ -66,11 +63,9 @@ func main() {
|
|||||||
flag.Var(&addRawReadable, "add-readable-raw", "Add a readable file (don't transform to its real path)")
|
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.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(&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.BoolVar(&memfile, "memfd", false, "Use memfd as exec file")
|
||||||
flag.StringVar(&runt, "runner", "ptrace", "Runner for the program (ptrace, ns, container)")
|
flag.StringVar(&runt, "runner", "ptrace", "Runner for the program (ptrace, ns, container)")
|
||||||
flag.BoolVar(&cred, "cred", false, "Generate credential for containers (uid=10000)")
|
flag.BoolVar(&cred, "cred", false, "Generate credential for containers (uid=10000)")
|
||||||
flag.BoolVar(&nucg, "nucg", false, "don't unshare cgroup")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
args = flag.Args()
|
args = flag.Args()
|
||||||
@ -123,14 +118,12 @@ func main() {
|
|||||||
c = runner.StatusRunnerError
|
c = runner.StatusRunnerError
|
||||||
}
|
}
|
||||||
// Handle fatal error from trace
|
// Handle fatal error from trace
|
||||||
fmt.Fprintf(f, "%d %d %d %d\n", getStatus(c),
|
fmt.Fprintf(f, "%d %d %d %d\n", getStatus(c), int(rt.Time/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
|
||||||
int(rt.Time.Round(time.Millisecond)/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
|
|
||||||
if c == runner.StatusRunnerError {
|
if c == runner.StatusRunnerError {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(f, "%d %d %d %d\n", 0,
|
fmt.Fprintf(f, "%d %d %d %d\n", 0, int(rt.Time/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
|
||||||
int(rt.Time.Round(time.Millisecond)/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,16 +132,14 @@ type containerRunner struct {
|
|||||||
container.ExecveParam
|
container.ExecveParam
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *containerRunner) Run(c context.Context) runner.Result {
|
func (r *containerRunner) Run(c context.Context) <-chan runner.Result {
|
||||||
return r.Environment.Execve(c, r.ExecveParam)
|
return r.Environment.Execve(c, r.ExecveParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() (*runner.Result, error) {
|
func start() (*runner.Result, error) {
|
||||||
var (
|
var (
|
||||||
r runner.Runner
|
r runner.Runner
|
||||||
cg cgroup.Cgroup
|
cg *cgroup.Cgroup
|
||||||
cgDir *os.File
|
|
||||||
cgroupFd uintptr
|
|
||||||
err error
|
err error
|
||||||
execFile uintptr
|
execFile uintptr
|
||||||
rt runner.Result
|
rt runner.Result
|
||||||
@ -180,68 +171,46 @@ func start() (*runner.Result, error) {
|
|||||||
// work dir
|
// work dir
|
||||||
WithTmpfs("w", "size=8m,nr_inodes=4k").
|
WithTmpfs("w", "size=8m,nr_inodes=4k").
|
||||||
// tmp dir
|
// tmp dir
|
||||||
WithTmpfs("tmp", "size=8m,nr_inodes=4k").
|
WithTmpfs("tmp", "size=8m,nr_inodes=4k")
|
||||||
FilterNotExist()
|
|
||||||
|
|
||||||
mt, err := mb.FilterNotExist().Build()
|
mt, err := mb.Build(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if useCGroup {
|
if useCGroup {
|
||||||
t := cgroup.DetectType()
|
b, err := cgroup.NewBuilder("runprog").WithCPUAcct().WithMemory().WithPids().FilterByEnv()
|
||||||
if t == cgroup.TypeV2 {
|
|
||||||
cgroup.EnableV2Nesting()
|
|
||||||
}
|
|
||||||
ct, err := cgroup.GetAvailableController()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b, err := cgroup.New("runprog", ct)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
debug(b)
|
debug(b)
|
||||||
cg, err = b.Random("runprog")
|
cg, err = b.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer cg.Destroy()
|
defer cg.Destroy()
|
||||||
if err = cg.SetMemoryLimit(memoryLimit << 20); err != nil {
|
if err = cg.SetMemoryLimitInBytes(memoryLimit << 20); err != nil {
|
||||||
return nil, err
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var syncFunc func(pid int) error
|
syncFunc := func(pid int) error {
|
||||||
if cg != nil {
|
if cg != nil {
|
||||||
syncFunc = func(pid int) error {
|
|
||||||
if err := cg.AddProc(pid); err != nil {
|
if err := cg.AddProc(pid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if memfile {
|
if memfile {
|
||||||
fin, err := os.Open(args[0])
|
fin, err := os.Open(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open args[0]: %w", err)
|
return nil, fmt.Errorf("filed to open args[0]: %v", err)
|
||||||
}
|
}
|
||||||
execf, err := memfd.DupToMemfd("run_program", fin)
|
execf, err := memfd.DupToMemfd("run_program", fin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("dup to memfd failed: %w", err)
|
return nil, fmt.Errorf("dup to memfd failed: %v", err)
|
||||||
}
|
}
|
||||||
fin.Close()
|
fin.Close()
|
||||||
defer execf.Close()
|
defer execf.Close()
|
||||||
@ -252,7 +221,7 @@ func start() (*runner.Result, error) {
|
|||||||
// open input / output / err files
|
// open input / output / err files
|
||||||
files, err := prepareFiles(inputFileName, outputFileName, errorFileName)
|
files, err := prepareFiles(inputFileName, outputFileName, errorFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to prepare files: %w", err)
|
return nil, fmt.Errorf("failed to prepare files: %v", err)
|
||||||
}
|
}
|
||||||
defer closeFiles(files)
|
defer closeFiles(files)
|
||||||
|
|
||||||
@ -271,32 +240,12 @@ func start() (*runner.Result, error) {
|
|||||||
CPUHard: realTimeLimit,
|
CPUHard: realTimeLimit,
|
||||||
FileSize: outputLimit << 20,
|
FileSize: outputLimit << 20,
|
||||||
Stack: stackLimit << 20,
|
Stack: stackLimit << 20,
|
||||||
Data: memoryLimit << 20,
|
|
||||||
OpenFile: 256,
|
|
||||||
DisableCore: true,
|
|
||||||
}
|
}
|
||||||
debug("rlimit: ", rlims)
|
debug("rlimit: ", rlims)
|
||||||
|
|
||||||
actionDefault := libseccomp.ActionKill
|
actionDefault := seccomp.ActionKill
|
||||||
if showDetails {
|
if showDetails {
|
||||||
actionDefault = libseccomp.ActionTrace
|
actionDefault = seccomp.ActionTrace.WithReturnCode(seccomp.MsgDisallow)
|
||||||
}
|
|
||||||
if runt != "ptrace" {
|
|
||||||
allow = append(allow, trace...)
|
|
||||||
trace = nil
|
|
||||||
}
|
|
||||||
builder := libseccomp.Builder{
|
|
||||||
Allow: allow,
|
|
||||||
Trace: trace,
|
|
||||||
Default: actionDefault,
|
|
||||||
}
|
|
||||||
// do not build filter for container unsafe since seccomp is not compatible with aarch64 syscalls
|
|
||||||
var filter seccomp.Filter
|
|
||||||
if !unsafe || runt != "container" {
|
|
||||||
filter, err = builder.Build()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create seccomp filter: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := runner.Limit{
|
limit := runner.Limit{
|
||||||
@ -305,39 +254,32 @@ func start() (*runner.Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if runt == "container" {
|
if runt == "container" {
|
||||||
|
root, err := ioutil.TempDir("", "dm")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot make temp root for container namespace: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(root)
|
||||||
|
|
||||||
var credG container.CredGenerator
|
var credG container.CredGenerator
|
||||||
if cred {
|
if cred {
|
||||||
credG = newCredGen()
|
credG = newCredGen()
|
||||||
}
|
}
|
||||||
var stderr io.Writer
|
|
||||||
if showDetails {
|
|
||||||
stderr = os.Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
cloneFlag := forkexec.UnshareFlags
|
|
||||||
if nucg {
|
|
||||||
cloneFlag &= ^unix.CLONE_NEWCGROUP
|
|
||||||
}
|
|
||||||
|
|
||||||
b := container.Builder{
|
b := container.Builder{
|
||||||
TmpRoot: "dm",
|
Root: root,
|
||||||
Mounts: mb.Mounts,
|
Mounts: mt,
|
||||||
Stderr: stderr,
|
|
||||||
CredGenerator: credG,
|
CredGenerator: credG,
|
||||||
CloneFlags: uintptr(cloneFlag),
|
CloneFlags: forkexec.UnshareFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := b.Build()
|
m, err := b.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to new container: %w", err)
|
return nil, fmt.Errorf("failed to new container: %v", err)
|
||||||
}
|
}
|
||||||
defer m.Destroy()
|
defer m.Destroy()
|
||||||
err = m.Ping()
|
err = m.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to ping container: %w", err)
|
return nil, fmt.Errorf("failed to ping container: %v", err)
|
||||||
}
|
|
||||||
if unsafe {
|
|
||||||
filter = nil
|
|
||||||
}
|
}
|
||||||
r = &containerRunner{
|
r = &containerRunner{
|
||||||
Environment: m,
|
Environment: m,
|
||||||
@ -347,14 +289,19 @@ func start() (*runner.Result, error) {
|
|||||||
Files: fds,
|
Files: fds,
|
||||||
ExecFile: execFile,
|
ExecFile: execFile,
|
||||||
RLimits: rlims.PrepareRLimit(),
|
RLimits: rlims.PrepareRLimit(),
|
||||||
Seccomp: filter,
|
|
||||||
SyncFunc: syncFunc,
|
SyncFunc: syncFunc,
|
||||||
CgroupFD: cgroupFd,
|
|
||||||
SyncAfterExec: cg == nil || cgDir != nil,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if runt == "ns" {
|
} else if runt == "ns" {
|
||||||
root, err := os.MkdirTemp("", "ns")
|
builder := libseccomp.Builder{
|
||||||
|
Allow: append(allow, trace...),
|
||||||
|
Default: actionDefault,
|
||||||
|
}
|
||||||
|
filter, err := builder.Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot build seccomp filter %v", err)
|
||||||
|
}
|
||||||
|
root, err := ioutil.TempDir("", "ns")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot make temp root for new namespace")
|
return nil, fmt.Errorf("cannot make temp root for new namespace")
|
||||||
}
|
}
|
||||||
@ -376,6 +323,15 @@ func start() (*runner.Result, error) {
|
|||||||
DomainName: "run_program",
|
DomainName: "run_program",
|
||||||
}
|
}
|
||||||
} else if runt == "ptrace" {
|
} else if runt == "ptrace" {
|
||||||
|
builder := libseccomp.Builder{
|
||||||
|
Allow: allow,
|
||||||
|
Trace: trace,
|
||||||
|
Default: actionDefault,
|
||||||
|
}
|
||||||
|
filter, err := builder.Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create seccomp filter %v", err)
|
||||||
|
}
|
||||||
r = &ptrace.Runner{
|
r = &ptrace.Runner{
|
||||||
Args: args,
|
Args: args,
|
||||||
Env: []string{pathEnv},
|
Env: []string{pathEnv},
|
||||||
@ -403,10 +359,7 @@ func start() (*runner.Result, error) {
|
|||||||
c, cancel := context.WithTimeout(context.Background(), time.Duration(int64(realTimeLimit)*int64(time.Second)))
|
c, cancel := context.WithTimeout(context.Background(), time.Duration(int64(realTimeLimit)*int64(time.Second)))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
s := make(chan runner.Result, 1)
|
s := r.Run(c)
|
||||||
go func() {
|
|
||||||
s <- r.Run(c)
|
|
||||||
}()
|
|
||||||
rTime := time.Now()
|
rTime := time.Now()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -427,45 +380,68 @@ func start() (*runner.Result, error) {
|
|||||||
debug("results:", rt, err)
|
debug("results:", rt, err)
|
||||||
|
|
||||||
if useCGroup {
|
if useCGroup {
|
||||||
cpu, err := cg.CPUUsage()
|
cpu, err := cg.CpuacctUsage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cgroup cpu: %v", err)
|
return nil, fmt.Errorf("cgroup cpu: %v", err)
|
||||||
} else {
|
|
||||||
rt.Time = time.Duration(cpu)
|
|
||||||
}
|
}
|
||||||
// max memory usage may not exist in cgroup v2
|
memory, err := cg.MemoryMaxUsageInBytes()
|
||||||
memory, err := cg.MemoryMaxUsage()
|
if err != nil {
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil, fmt.Errorf("cgroup memory: %v", err)
|
return nil, fmt.Errorf("cgroup memory: %v", err)
|
||||||
} else if err == nil {
|
|
||||||
rt.Memory = runner.Size(memory)
|
|
||||||
}
|
}
|
||||||
procPeak, err := cg.ProcessPeak()
|
cache, err := cg.FindMemoryStatProperty("cache")
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cgroup pid: %v", err)
|
return nil, fmt.Errorf("cgroup cache %v", err)
|
||||||
} else if err == nil {
|
|
||||||
rt.ProcPeak = procPeak
|
|
||||||
}
|
}
|
||||||
debug("cgroup: cpu: ", cpu, " memory: ", memory, " procPeak: ", procPeak)
|
debug("cgroup: cpu: ", cpu, " memory: ", memory, "cache: ", cache)
|
||||||
|
rt.Time = time.Duration(cpu)
|
||||||
|
rt.Memory = runner.Size(memory - cache)
|
||||||
debug("cgroup:", rt)
|
debug("cgroup:", rt)
|
||||||
}
|
}
|
||||||
if rt.Status == runner.StatusTimeLimitExceeded || rt.Status == runner.StatusNormal {
|
|
||||||
if rt.Time > limit.TimeLimit {
|
|
||||||
rt.Status = runner.StatusTimeLimitExceeded
|
|
||||||
} else {
|
|
||||||
rt.Status = runner.StatusNormal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rt.Status == runner.StatusMemoryLimitExceeded || rt.Status == runner.StatusNormal {
|
|
||||||
if rt.Memory > limit.MemoryLimit {
|
|
||||||
rt.Status = runner.StatusMemoryLimitExceeded
|
|
||||||
} else {
|
|
||||||
rt.Status = runner.StatusNormal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &rt, nil
|
return &rt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func debug(v ...interface{}) {
|
||||||
|
if showDetails {
|
||||||
|
fmt.Fprintln(os.Stderr, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status defines uoj/run_program constants
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
// UOJ run_program constants
|
||||||
|
const (
|
||||||
|
StatusNormal Status = iota // 0
|
||||||
|
StatusInvalid // 1
|
||||||
|
StatusRE // 2
|
||||||
|
StatusMLE // 3
|
||||||
|
StatusTLE // 4
|
||||||
|
StatusOLE // 5
|
||||||
|
StatusBan // 6
|
||||||
|
StatusFatal // 7
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStatus(s runner.Status) int {
|
||||||
|
switch s {
|
||||||
|
case runner.StatusNormal:
|
||||||
|
return int(StatusNormal)
|
||||||
|
case runner.StatusInvalid:
|
||||||
|
return int(StatusInvalid)
|
||||||
|
case runner.StatusTimeLimitExceeded:
|
||||||
|
return int(StatusTLE)
|
||||||
|
case runner.StatusMemoryLimitExceeded:
|
||||||
|
return int(StatusMLE)
|
||||||
|
case runner.StatusOutputLimitExceeded:
|
||||||
|
return int(StatusOLE)
|
||||||
|
case runner.StatusDisallowedSyscall:
|
||||||
|
return int(StatusBan)
|
||||||
|
case runner.StatusSignalled, runner.StatusNonzeroExitStatus:
|
||||||
|
return int(StatusRE)
|
||||||
|
default:
|
||||||
|
return int(StatusFatal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type credGen struct {
|
type credGen struct {
|
||||||
cur uint32
|
cur uint32
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,12 +32,11 @@ var (
|
|||||||
"fstat",
|
"fstat",
|
||||||
"lseek",
|
"lseek",
|
||||||
"dup",
|
"dup",
|
||||||
|
"dup2",
|
||||||
"dup3",
|
"dup3",
|
||||||
"ioctl",
|
"ioctl",
|
||||||
"fcntl",
|
"fcntl",
|
||||||
"fadvise64",
|
"fadvise64",
|
||||||
"pread64",
|
|
||||||
"pwrite64",
|
|
||||||
|
|
||||||
// memory action
|
// memory action
|
||||||
"mmap",
|
"mmap",
|
||||||
@ -64,10 +63,13 @@ var (
|
|||||||
"exit_group",
|
"exit_group",
|
||||||
|
|
||||||
// others
|
// others
|
||||||
|
"arch_prctl",
|
||||||
|
|
||||||
"gettimeofday",
|
"gettimeofday",
|
||||||
"getrlimit",
|
"getrlimit",
|
||||||
"getrusage",
|
"getrusage",
|
||||||
"times",
|
"times",
|
||||||
|
"time",
|
||||||
"clock_gettime",
|
"clock_gettime",
|
||||||
|
|
||||||
"restart_syscall",
|
"restart_syscall",
|
||||||
@ -80,15 +82,21 @@ var (
|
|||||||
"execveat",
|
"execveat",
|
||||||
|
|
||||||
// file open
|
// file open
|
||||||
|
"open",
|
||||||
"openat",
|
"openat",
|
||||||
|
|
||||||
// file delete
|
// file delete
|
||||||
|
"unlink",
|
||||||
"unlinkat",
|
"unlinkat",
|
||||||
|
|
||||||
// soft link
|
// soft link
|
||||||
|
"readlink",
|
||||||
"readlinkat",
|
"readlinkat",
|
||||||
|
|
||||||
// permission check
|
// permission check
|
||||||
|
"lstat",
|
||||||
|
"stat",
|
||||||
|
"access",
|
||||||
"faccessat",
|
"faccessat",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +180,7 @@ var (
|
|||||||
"sched_getaffinity", "sched_yield",
|
"sched_getaffinity", "sched_yield",
|
||||||
"uname", "sysinfo",
|
"uname", "sysinfo",
|
||||||
"prlimit64", "getrandom",
|
"prlimit64", "getrandom",
|
||||||
"fchmodat", "rseq",
|
"fchmodat",
|
||||||
},
|
},
|
||||||
ExtraBan: []string{"socket", "connect", "geteuid", "getuid"},
|
ExtraBan: []string{"socket", "connect", "geteuid", "getuid"},
|
||||||
},
|
},
|
||||||
14
config/config_amd64.go
Normal file
14
config/config_amd64.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// This file includes configs for the run program settings
|
||||||
|
|
||||||
|
var (
|
||||||
|
archReadableFiles = []string{
|
||||||
|
"/lib/x86_64-linux-gnu/",
|
||||||
|
"/usr/lib/x86_64-linux-gnu/",
|
||||||
|
}
|
||||||
|
|
||||||
|
archSyscallAllows = []string{}
|
||||||
|
|
||||||
|
archSyscallTraces = []string{}
|
||||||
|
)
|
||||||
@ -17,19 +17,10 @@ var (
|
|||||||
"uname",
|
"uname",
|
||||||
"set_tls",
|
"set_tls",
|
||||||
"arm_fadvise64_64",
|
"arm_fadvise64_64",
|
||||||
"dup2",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
archSyscallTraces = []string{
|
archSyscallTraces = []string{
|
||||||
"lstat64", // 32-bit
|
"lstat64", // 32-bit
|
||||||
"stat64", // 32-bit
|
"stat64", // 32-bit
|
||||||
"open",
|
|
||||||
"unlink",
|
|
||||||
"readlink",
|
|
||||||
"lstat",
|
|
||||||
"stat",
|
|
||||||
"access",
|
|
||||||
"fstatat",
|
|
||||||
"fstatat64",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -8,9 +8,9 @@ var (
|
|||||||
"/usr/lib/aarch64-linux-gnu/",
|
"/usr/lib/aarch64-linux-gnu/",
|
||||||
}
|
}
|
||||||
|
|
||||||
archSyscallAllows = []string{}
|
archSyscallAllows = []string{
|
||||||
|
"newfstatat",
|
||||||
archSyscallTraces = []string{
|
|
||||||
"fstatat",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
archSyscallTraces = []string{}
|
||||||
)
|
)
|
||||||
@ -3,9 +3,7 @@ package container
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"io/ioutil"
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/runner"
|
"github.com/criyle/go-sandbox/runner"
|
||||||
@ -16,172 +14,102 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkContainer(b *testing.B) {
|
func BenchmarkContainer(b *testing.B) {
|
||||||
tmpDir, err := os.MkdirTemp("", "")
|
tmpDir, err := ioutil.TempDir("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(err)
|
b.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
builder := &Builder{
|
builder := &Builder{
|
||||||
Root: tmpDir,
|
Root: tmpDir,
|
||||||
Stderr: os.Stderr,
|
|
||||||
}
|
}
|
||||||
n := runtime.GOMAXPROCS(0)
|
|
||||||
ch := make(chan Environment, n)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
m, err := builder.Build()
|
m, err := builder.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(err)
|
b.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
b.Cleanup(func() {
|
b.Cleanup(func() {
|
||||||
m.Destroy()
|
m.Destroy()
|
||||||
})
|
})
|
||||||
ch <- m
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
for i := 0; i < b.N; i++ {
|
||||||
m := <-ch
|
rt := m.Execve(context.TODO(), ExecveParam{
|
||||||
for pb.Next() {
|
Args: []string{"/bin/echo"},
|
||||||
r := m.Execve(context.TODO(), ExecveParam{
|
|
||||||
Args: []string{"/bin/true"},
|
|
||||||
Env: []string{"PATH=/bin"},
|
Env: []string{"PATH=/bin"},
|
||||||
})
|
})
|
||||||
|
r := <-rt
|
||||||
if r.Status != runner.StatusNormal {
|
if r.Status != runner.StatusNormal {
|
||||||
b.Error(r.Status, r.Error)
|
b.Error(r.Status, r.Error)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerSuccess(t *testing.T) {
|
||||||
|
m := getEnv(t)
|
||||||
|
if m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rt := m.Execve(context.TODO(), ExecveParam{
|
||||||
|
Args: []string{"/bin/echo"},
|
||||||
|
Env: []string{"PATH=/bin"},
|
||||||
})
|
})
|
||||||
}
|
r := <-rt
|
||||||
|
|
||||||
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{}
|
|
||||||
|
|
||||||
func (c credgen) Get() syscall.Credential {
|
|
||||||
return syscall.Credential{
|
|
||||||
Uid: 10000,
|
|
||||||
Gid: 10000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContainerSetCred(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
if os.Getpid() != 1 {
|
|
||||||
t.Skip("root required for this test")
|
|
||||||
}
|
|
||||||
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 {
|
if r.Status != runner.StatusNormal {
|
||||||
t.Fatal(r.Status, r.Error, r)
|
t.Error(r.Status, r.Error)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCases(t *testing.T) {
|
func TestContainerNotExists(t *testing.T) {
|
||||||
for _, c := range tests {
|
m := getEnv(t)
|
||||||
t.Run(c.name, func(t *testing.T) {
|
if m == nil {
|
||||||
runTest(t, c.param, c.expected, nil)
|
return
|
||||||
|
}
|
||||||
|
rt := m.Execve(context.TODO(), ExecveParam{
|
||||||
|
Args: []string{"not_exists"},
|
||||||
|
Env: []string{"PATH=/bin"},
|
||||||
})
|
})
|
||||||
|
r := <-rt
|
||||||
|
if r.Status != runner.StatusRunnerError {
|
||||||
|
t.Error(r.Status, r.Error)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnv(t *testing.T, credGen CredGenerator) Environment {
|
func TestContainerSyncFuncFail(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "")
|
m := getEnv(t)
|
||||||
|
if m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := errors.New("test error")
|
||||||
|
rt := m.Execve(context.TODO(), ExecveParam{
|
||||||
|
Args: []string{"/bin/echo"},
|
||||||
|
Env: []string{"PATH=/bin"},
|
||||||
|
SyncFunc: func(pid int) error {
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
})
|
||||||
|
r := <-rt
|
||||||
|
if r.Status != runner.StatusRunnerError {
|
||||||
|
t.Error(r.Status, r.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(t *testing.T) Environment {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Error(err)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
t.Cleanup(func() {
|
|
||||||
os.Remove(tmpDir)
|
|
||||||
})
|
|
||||||
builder := &Builder{
|
builder := &Builder{
|
||||||
Root: tmpDir,
|
Root: tmpDir,
|
||||||
CredGenerator: credGen,
|
|
||||||
Stderr: os.Stderr,
|
|
||||||
}
|
}
|
||||||
m, err := builder.Build()
|
m, err := builder.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Error(err)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
t.Cleanup(func() {
|
|
||||||
m.Destroy()
|
|
||||||
})
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|||||||
25
container/consts.go
Normal file
25
container/consts.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
const (
|
||||||
|
cmdPing = "ping"
|
||||||
|
cmdCopyIn = "copyin"
|
||||||
|
cmdOpen = "open"
|
||||||
|
cmdDelete = "delete"
|
||||||
|
cmdReset = "reset"
|
||||||
|
cmdExecve = "execve"
|
||||||
|
cmdOk = "ok"
|
||||||
|
cmdKill = "kill"
|
||||||
|
cmdConf = "conf"
|
||||||
|
|
||||||
|
initArg = "init"
|
||||||
|
|
||||||
|
currentExec = "/proc/self/exe"
|
||||||
|
|
||||||
|
containerUID = 1000
|
||||||
|
containerGID = 1000
|
||||||
|
|
||||||
|
containerName = "go-sandbox"
|
||||||
|
containerWD = "/w"
|
||||||
|
|
||||||
|
containerMaxProc = 1
|
||||||
|
)
|
||||||
@ -1,47 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
type cmdType int8
|
|
||||||
|
|
||||||
const (
|
|
||||||
cmdPing cmdType = iota + 1
|
|
||||||
cmdOpen
|
|
||||||
cmdDelete
|
|
||||||
cmdReset
|
|
||||||
cmdExecve
|
|
||||||
cmdOk
|
|
||||||
cmdKill
|
|
||||||
cmdConf
|
|
||||||
|
|
||||||
initArg = "container_init"
|
|
||||||
|
|
||||||
containerUID = 1000
|
|
||||||
containerGID = 1000
|
|
||||||
|
|
||||||
containerName = "go-sandbox"
|
|
||||||
containerWD = "/w"
|
|
||||||
|
|
||||||
containerMaxProc = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultSymLinks = []SymbolicLink{
|
|
||||||
{LinkPath: "/dev/fd", Target: "/proc/self/fd"},
|
|
||||||
{LinkPath: "/dev/stdin", Target: "/proc/self/fd/0"},
|
|
||||||
{LinkPath: "/dev/stdout", Target: "/proc/self/fd/1"},
|
|
||||||
{LinkPath: "/dev/stderr", Target: "/proc/self/fd/2"},
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultMaskPaths = []string{
|
|
||||||
// https://github.com/containerd/containerd/blob/f0a32c66dad1e9de716c9960af806105d691cd78/oci/spec.go#L165-L176
|
|
||||||
"/proc/acpi",
|
|
||||||
"/proc/asound",
|
|
||||||
"/proc/kcore",
|
|
||||||
"/proc/keys",
|
|
||||||
"/proc/latency_stats",
|
|
||||||
"/proc/timer_list",
|
|
||||||
"/proc/timer_stats",
|
|
||||||
"/proc/sched_debug",
|
|
||||||
"/sys/firmware",
|
|
||||||
"/proc/scsi",
|
|
||||||
|
|
||||||
"/usr/lib/wsl",
|
|
||||||
}
|
|
||||||
@ -1,39 +1,22 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"syscall"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *containerServer) handlePing() error {
|
func (c *containerServer) handlePing() error {
|
||||||
return c.sendReply(reply{}, unixsocket.Msg{})
|
return c.sendReply(&reply{}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerServer) handleConf(conf *confCmd) error {
|
func (c *containerServer) handleConf(conf *confCmd) error {
|
||||||
if conf != nil {
|
if conf != nil {
|
||||||
c.containerConfig = conf.Conf
|
c.containerConfig = conf.Conf
|
||||||
if err := initContainer(conf.Conf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if c.ContainerUID == 0 {
|
return c.sendReply(&reply{}, nil)
|
||||||
c.ContainerUID = containerUID
|
|
||||||
}
|
|
||||||
if c.ContainerGID == 0 {
|
|
||||||
c.ContainerGID = containerGID
|
|
||||||
}
|
|
||||||
env, err := readDotEnv()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.defaultEnv = env
|
|
||||||
}
|
|
||||||
return c.sendReply(reply{}, unixsocket.Msg{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerServer) handleOpen(open []OpenCmd) error {
|
func (c *containerServer) handleOpen(open []OpenCmd) error {
|
||||||
@ -43,20 +26,16 @@ func (c *containerServer) handleOpen(open []OpenCmd) error {
|
|||||||
|
|
||||||
// open files
|
// open files
|
||||||
fds := make([]int, 0, len(open))
|
fds := make([]int, 0, len(open))
|
||||||
fileToClose := make([]*os.File, 0, len(open)) // let sendMsg close these files
|
|
||||||
for _, o := range open {
|
for _, o := range open {
|
||||||
outFile, err := os.OpenFile(o.Path, o.Flag, o.Perm)
|
outFile, err := os.OpenFile(o.Path, o.Flag, o.Perm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, f := range fileToClose {
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
return c.sendErrorReply("open: %v", err)
|
return c.sendErrorReply("open: %v", err)
|
||||||
}
|
}
|
||||||
fileToClose = append(fileToClose, outFile)
|
defer outFile.Close()
|
||||||
fds = append(fds, int(outFile.Fd()))
|
fds = append(fds, int(outFile.Fd()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.sendReplyFiles(reply{}, unixsocket.Msg{Fds: fds}, fileToClose)
|
return c.sendReply(&reply{}, &unixsocket.Msg{Fds: fds})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerServer) handleDelete(delete *deleteCmd) error {
|
func (c *containerServer) handleDelete(delete *deleteCmd) error {
|
||||||
@ -66,43 +45,42 @@ func (c *containerServer) handleDelete(delete *deleteCmd) error {
|
|||||||
if err := os.Remove(delete.Path); err != nil {
|
if err := os.Remove(delete.Path); err != nil {
|
||||||
return c.sendErrorReply("delete: %v", err)
|
return c.sendErrorReply("delete: %v", err)
|
||||||
}
|
}
|
||||||
return c.sendReply(reply{}, unixsocket.Msg{})
|
return c.sendReply(&reply{}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerServer) handleReset() error {
|
func (c *containerServer) handleReset() error {
|
||||||
for _, m := range c.Mounts {
|
if err := removeContents("/tmp"); err != nil {
|
||||||
if !m.IsTmpFs() {
|
return c.sendErrorReply("reset: /tmp %v", err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if err := removeContents(filepath.Join("/", m.Target)); err != nil {
|
if err := removeContents("/w"); err != nil {
|
||||||
return c.sendErrorReply("reset: %v %v", m.Target, err)
|
return c.sendErrorReply("reset: /w %v", err)
|
||||||
}
|
}
|
||||||
}
|
return c.sendReply(&reply{}, nil)
|
||||||
return c.sendReply(reply{}, unixsocket.Msg{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readDotEnv attempts to read /.env file and save as default environment variables
|
func (c *containerServer) recvCmd() (*cmd, *unixsocket.Msg, error) {
|
||||||
func readDotEnv() ([]string, error) {
|
cm := new(cmd)
|
||||||
f, err := os.Open("/.env")
|
msg, err := c.socket.RecvMsg(cm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
return nil, nil, err
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("dotenv: open /.env: %w", err)
|
return cm, msg, nil
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
func (c *containerServer) sendReply(rep *reply, msg *unixsocket.Msg) error {
|
||||||
var ret []string
|
return c.socket.SendMsg(rep, msg)
|
||||||
scanner := bufio.NewScanner(f)
|
}
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
// sendErrorReply sends error reply
|
||||||
if len(line) == 0 || strings.HasPrefix(line, "#") {
|
func (c *containerServer) sendErrorReply(ft string, v ...interface{}) error {
|
||||||
continue
|
errRep := &errorReply{
|
||||||
}
|
Msg: fmt.Sprintf(ft, v...),
|
||||||
if !strings.Contains(line, "=") {
|
}
|
||||||
return nil, fmt.Errorf("dotenv: invalid line: %s", line)
|
// store errno
|
||||||
}
|
if len(v) == 1 {
|
||||||
ret = append(ret, line)
|
if errno, ok := v[0].(syscall.Errno); ok {
|
||||||
}
|
errRep.Errno = &errno
|
||||||
return ret, nil
|
}
|
||||||
|
}
|
||||||
|
return c.sendReply(&reply{Error: errRep}, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,17 +10,16 @@ import (
|
|||||||
"github.com/criyle/go-sandbox/runner"
|
"github.com/criyle/go-sandbox/runner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *containerServer) handleExecve(cmd *execCmd, msg unixsocket.Msg) error {
|
func (c *containerServer) handleExecve(cmd *execCmd, msg *unixsocket.Msg) error {
|
||||||
var (
|
var (
|
||||||
files []uintptr
|
files []uintptr
|
||||||
execFile uintptr
|
execFile uintptr
|
||||||
cgroupFd uintptr
|
|
||||||
cred *syscall.Credential
|
cred *syscall.Credential
|
||||||
)
|
)
|
||||||
if cmd == nil {
|
if cmd == nil {
|
||||||
return c.sendErrorReply("handle: no parameter provided")
|
return c.sendErrorReply("execve: no parameter provided")
|
||||||
}
|
}
|
||||||
if len(msg.Fds) > 0 {
|
if msg != nil {
|
||||||
files = intSliceToUintptr(msg.Fds)
|
files = intSliceToUintptr(msg.Fds)
|
||||||
// don't leak fds to child
|
// don't leak fds to child
|
||||||
closeOnExecFds(msg.Fds)
|
closeOnExecFds(msg.Fds)
|
||||||
@ -31,182 +30,116 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg unixsocket.Msg) error {
|
|||||||
// if fexecve, then the first fd must be executable
|
// if fexecve, then the first fd must be executable
|
||||||
if cmd.FdExec {
|
if cmd.FdExec {
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return c.sendErrorReply("handle: expected fexecve fd")
|
return fmt.Errorf("execve: expected fexecve fd")
|
||||||
}
|
}
|
||||||
execFile = files[0]
|
execFile = files[0]
|
||||||
files = files[1:]
|
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
|
syncFunc := func(pid int) error {
|
||||||
env = append(env, c.defaultEnv...)
|
msg := &unixsocket.Msg{
|
||||||
env = append(env, cmd.Env...)
|
|
||||||
|
|
||||||
if len(cmd.Argv) > 0 {
|
|
||||||
exePath, err := lookPath(cmd.Argv[0], env)
|
|
||||||
if err != nil {
|
|
||||||
return c.sendErrorReply("handle: %s: %v", cmd.Argv[0], err)
|
|
||||||
}
|
|
||||||
cmd.Argv[0] = exePath
|
|
||||||
}
|
|
||||||
|
|
||||||
syncPid := func(pid int) error {
|
|
||||||
msg := unixsocket.Msg{
|
|
||||||
Cred: &syscall.Ucred{
|
Cred: &syscall.Ucred{
|
||||||
Pid: int32(pid),
|
Pid: int32(pid),
|
||||||
Uid: uint32(syscall.Getuid()),
|
Uid: uint32(syscall.Getuid()),
|
||||||
Gid: uint32(syscall.Getgid()),
|
Gid: uint32(syscall.Getgid()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := c.sendReply(reply{}, msg); err != nil {
|
if err := c.sendReply(&reply{}, msg); err != nil {
|
||||||
return fmt.Errorf("sync func: send reply: %w", err)
|
return fmt.Errorf("syncFunc: sendReply %v", err)
|
||||||
}
|
}
|
||||||
cmd, _, err := c.recvCmd()
|
cmd, _, err := c.recvCmd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("sync func: recv cmd: %w", err)
|
return fmt.Errorf("syncFunc: recvCmd %v", err)
|
||||||
}
|
}
|
||||||
if cmd.Cmd == cmdKill {
|
if cmd.Cmd == cmdKill {
|
||||||
return fmt.Errorf("sync func: received kill")
|
return fmt.Errorf("syncFunc: received kill")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var syncFunc func(pid int) error
|
|
||||||
if !cmd.SyncAfter {
|
|
||||||
syncFunc = syncPid
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Cred {
|
if c.Cred {
|
||||||
cred = &syscall.Credential{
|
cred = &syscall.Credential{
|
||||||
Uid: uint32(c.ContainerUID),
|
Uid: containerUID,
|
||||||
Gid: uint32(c.ContainerGID),
|
Gid: containerGID,
|
||||||
NoSetGroups: true,
|
NoSetGroups: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var seccomp *syscall.SockFprog
|
|
||||||
if cmd.Seccomp != nil {
|
|
||||||
seccomp = cmd.Seccomp.SockFprog()
|
|
||||||
}
|
|
||||||
|
|
||||||
r := forkexec.Runner{
|
r := forkexec.Runner{
|
||||||
Args: cmd.Argv,
|
Args: cmd.Argv,
|
||||||
Env: env,
|
Env: cmd.Env,
|
||||||
ExecFile: execFile,
|
ExecFile: execFile,
|
||||||
RLimits: cmd.RLimits,
|
RLimits: cmd.RLimits,
|
||||||
Files: files,
|
Files: files,
|
||||||
WorkDir: c.WorkDir,
|
WorkDir: "/w",
|
||||||
NoNewPrivs: true,
|
NoNewPrivs: true,
|
||||||
DropCaps: true,
|
DropCaps: true,
|
||||||
SyncFunc: syncFunc,
|
SyncFunc: syncFunc,
|
||||||
Credential: cred,
|
Credential: cred,
|
||||||
CTTY: cmd.CTTY,
|
|
||||||
Seccomp: seccomp,
|
|
||||||
CgroupFd: cgroupFd,
|
|
||||||
|
|
||||||
UnshareCgroupAfterSync: c.UnshareCgroup,
|
UnshareCgroupAfterSync: true,
|
||||||
}
|
}
|
||||||
// starts the runner, error is handled same as wait4 to make communication equal
|
// starts the runner, error is handled same as wait4 to make communication equal
|
||||||
pid, err := r.Start()
|
pid, err := r.Start()
|
||||||
|
|
||||||
|
// done is to signal kill goroutine exits
|
||||||
|
killDone := make(chan struct{})
|
||||||
|
// waitDone is to signal kill goroutine to collect zombies
|
||||||
|
waitDone := make(chan struct{})
|
||||||
|
|
||||||
|
// recv kill
|
||||||
|
go func() {
|
||||||
|
// signal done
|
||||||
|
defer close(killDone)
|
||||||
|
// msg must be kill
|
||||||
|
c.recvCmd()
|
||||||
|
// kill all
|
||||||
|
syscall.Kill(-1, syscall.SIGKILL)
|
||||||
|
// make sure collect zombie does not consume the exit status
|
||||||
|
<-waitDone
|
||||||
|
// collect zombies
|
||||||
|
for {
|
||||||
|
if _, err := syscall.Wait4(-1, nil, syscall.WNOHANG, nil); err != nil && err != syscall.EINTR {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait pid if no error encountered for execve
|
||||||
|
var wstatus syscall.WaitStatus
|
||||||
|
var rusage syscall.Rusage
|
||||||
|
if err == nil {
|
||||||
|
_, err = syscall.Wait4(pid, &wstatus, 0, &rusage)
|
||||||
|
for err == syscall.EINTR {
|
||||||
|
_, err = syscall.Wait4(pid, &wstatus, 0, &rusage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sync with kill goroutine
|
||||||
|
close(waitDone)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s := "<nil>"
|
c.sendErrorReply("execve: wait4 %v", err)
|
||||||
if len(cmd.Argv) > 0 {
|
} else {
|
||||||
s = cmd.Argv[0]
|
|
||||||
}
|
|
||||||
return c.sendErrorReply("start: %s: %v", s, err)
|
|
||||||
}
|
|
||||||
if cmd.SyncAfter {
|
|
||||||
if err := syncPid(1); err != nil {
|
|
||||||
syscall.Kill(-1, syscall.SIGKILL)
|
|
||||||
|
|
||||||
c.waitPid <- pid
|
|
||||||
ret := <-c.waitPidResult
|
|
||||||
err := c.sendReply(convertReply(ret), unixsocket.Msg{})
|
|
||||||
|
|
||||||
c.waitAll <- struct{}{}
|
|
||||||
<-c.waitAllDone
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.handleExecveStarted(pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerServer) handleExecveStarted(pid int) error {
|
|
||||||
// At this point, either recv kill / send result would be happened
|
|
||||||
// host -> container: kill
|
|
||||||
// container -> host: result
|
|
||||||
// container -> host: done
|
|
||||||
|
|
||||||
// Let's register a wait event
|
|
||||||
c.waitPid <- pid
|
|
||||||
|
|
||||||
var ret waitPidResult
|
|
||||||
select {
|
|
||||||
case <-c.done: // socket error happened
|
|
||||||
return c.err
|
|
||||||
|
|
||||||
case <-c.recvCh: // kill cmd received
|
|
||||||
syscall.Kill(-1, syscall.SIGKILL)
|
|
||||||
ret = <-c.waitPidResult
|
|
||||||
c.waitAll <- struct{}{}
|
|
||||||
|
|
||||||
if err := c.sendReply(convertReply(ret), unixsocket.Msg{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case ret = <-c.waitPidResult: // child process returned
|
|
||||||
syscall.Kill(-1, syscall.SIGKILL)
|
|
||||||
c.waitAll <- struct{}{}
|
|
||||||
|
|
||||||
if err := c.sendReply(convertReply(ret), unixsocket.Msg{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, _, err := c.recvCmd(); err != nil { // kill cmd received
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<-c.waitAllDone
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertReply(ret waitPidResult) reply {
|
|
||||||
if ret.Err != nil {
|
|
||||||
return reply{
|
|
||||||
Error: &errorReply{
|
|
||||||
Msg: fmt.Sprintf("execve: wait4: %v", ret.Err),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
waitStatus := ret.WaitStatus
|
|
||||||
rusage := ret.Rusage
|
|
||||||
|
|
||||||
status := runner.StatusNormal
|
status := runner.StatusNormal
|
||||||
userTime := time.Duration(rusage.Utime.Nano()) // ns
|
userTime := time.Duration(rusage.Utime.Nano()) // ns
|
||||||
userMem := runner.Size(rusage.Maxrss << 10) // bytes
|
userMem := runner.Size(rusage.Maxrss << 10) // bytes
|
||||||
switch {
|
switch {
|
||||||
case waitStatus.Exited():
|
case wstatus.Exited():
|
||||||
exitStatus := waitStatus.ExitStatus()
|
exitStatus := wstatus.ExitStatus()
|
||||||
if exitStatus != 0 {
|
if exitStatus != 0 {
|
||||||
status = runner.StatusNonzeroExitStatus
|
status = runner.StatusNonzeroExitStatus
|
||||||
}
|
}
|
||||||
return reply{
|
c.sendReply(&reply{
|
||||||
ExecReply: &execReply{
|
ExecReply: &execReply{
|
||||||
Status: status,
|
Status: status,
|
||||||
ExitStatus: exitStatus,
|
ExitStatus: exitStatus,
|
||||||
Time: userTime,
|
Time: userTime,
|
||||||
Memory: userMem,
|
Memory: userMem,
|
||||||
},
|
},
|
||||||
}
|
}, nil)
|
||||||
|
|
||||||
case waitStatus.Signaled():
|
case wstatus.Signaled():
|
||||||
switch waitStatus.Signal() {
|
switch wstatus.Signal() {
|
||||||
// kill signal treats as TLE
|
// kill signal treats as TLE
|
||||||
case syscall.SIGXCPU, syscall.SIGKILL:
|
case syscall.SIGXCPU, syscall.SIGKILL:
|
||||||
status = runner.StatusTimeLimitExceeded
|
status = runner.StatusTimeLimitExceeded
|
||||||
@ -217,20 +150,21 @@ func convertReply(ret waitPidResult) reply {
|
|||||||
default:
|
default:
|
||||||
status = runner.StatusSignalled
|
status = runner.StatusSignalled
|
||||||
}
|
}
|
||||||
return reply{
|
c.sendReply(&reply{
|
||||||
ExecReply: &execReply{
|
ExecReply: &execReply{
|
||||||
ExitStatus: int(waitStatus.Signal()),
|
ExitStatus: int(wstatus.Signal()),
|
||||||
Status: status,
|
Status: status,
|
||||||
Time: userTime,
|
Time: userTime,
|
||||||
Memory: userMem,
|
Memory: userMem,
|
||||||
},
|
},
|
||||||
}
|
}, nil)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return reply{
|
c.sendErrorReply("execve: unknown status %v", wstatus)
|
||||||
Error: &errorReply{
|
|
||||||
Msg: fmt.Sprintf("execve: unknown status: %v", waitStatus),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wait for kill msg and reply done for finish
|
||||||
|
<-killDone
|
||||||
|
return c.sendReply(&reply{}, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,9 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
||||||
)
|
)
|
||||||
@ -18,37 +11,6 @@ import (
|
|||||||
type containerServer struct {
|
type containerServer struct {
|
||||||
socket *socket
|
socket *socket
|
||||||
containerConfig
|
containerConfig
|
||||||
defaultEnv []string
|
|
||||||
|
|
||||||
done chan struct{}
|
|
||||||
err error
|
|
||||||
doneOnce sync.Once
|
|
||||||
|
|
||||||
recvCh chan recvCmd
|
|
||||||
sendCh chan sendReply
|
|
||||||
|
|
||||||
waitPid chan int
|
|
||||||
waitPidResult chan waitPidResult
|
|
||||||
|
|
||||||
waitAll chan struct{}
|
|
||||||
waitAllDone chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type recvCmd struct {
|
|
||||||
Cmd cmd
|
|
||||||
Msg unixsocket.Msg
|
|
||||||
}
|
|
||||||
|
|
||||||
type sendReply struct {
|
|
||||||
Reply reply
|
|
||||||
Msg unixsocket.Msg
|
|
||||||
FileToClose []*os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
type waitPidResult struct {
|
|
||||||
WaitStatus syscall.WaitStatus
|
|
||||||
Rusage syscall.Rusage
|
|
||||||
Err error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init is called for container init process
|
// Init is called for container init process
|
||||||
@ -58,7 +20,7 @@ type waitPidResult struct {
|
|||||||
func Init() (err error) {
|
func Init() (err error) {
|
||||||
// noop if self is not container init process
|
// noop if self is not container init process
|
||||||
// Notice: docker init is also 1, additional check for args[1] == init
|
// Notice: docker init is also 1, additional check for args[1] == init
|
||||||
if os.Getpid() != 1 || len(os.Args) < 2 || os.Args[1] != initArg {
|
if os.Getpid() != 1 || len(os.Args) != 2 || os.Args[1] != initArg {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,132 +42,34 @@ func Init() (err error) {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// ignore any signal that kills the init process
|
|
||||||
ignoreSignals()
|
|
||||||
|
|
||||||
// limit container resource usage
|
// limit container resource usage
|
||||||
runtime.GOMAXPROCS(containerMaxProc)
|
runtime.GOMAXPROCS(containerMaxProc)
|
||||||
|
|
||||||
// ensure there's no fd leak to child process (e.g. VSCode leaks ptmx fd)
|
|
||||||
if err := closeOnExecAllFds(); err != nil {
|
|
||||||
return fmt.Errorf("container_init: failed to close on exec all fds: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// new_container environment shared the socket at fd 3 (marked close_exec)
|
// new_container environment shared the socket at fd 3 (marked close_exec)
|
||||||
const defaultFd = 3
|
const defaultFd = 3
|
||||||
soc, err := unixsocket.NewSocket(defaultFd)
|
soc, err := unixsocket.NewSocket(defaultFd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("container_init: failed to create new socket: %w", err)
|
return fmt.Errorf("container_init: failed to new socket %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve forever
|
// serve forever
|
||||||
cs := &containerServer{
|
cs := &containerServer{socket: newSocket(soc)}
|
||||||
socket: newSocket(soc),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
sendCh: make(chan sendReply, 1),
|
|
||||||
recvCh: make(chan recvCmd, 1),
|
|
||||||
waitPid: make(chan int),
|
|
||||||
waitAll: make(chan struct{}),
|
|
||||||
waitPidResult: make(chan waitPidResult, 1),
|
|
||||||
waitAllDone: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
go cs.sendLoop()
|
|
||||||
go cs.recvLoop()
|
|
||||||
go cs.waitLoop()
|
|
||||||
|
|
||||||
return cs.serve()
|
return cs.serve()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerServer) sendLoop() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return
|
|
||||||
|
|
||||||
case rep, ok := <-c.sendCh:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := c.socket.SendMsg(rep.Reply, rep.Msg)
|
|
||||||
for _, f := range rep.FileToClose {
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.socketError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerServer) recvLoop() {
|
|
||||||
for {
|
|
||||||
var cmd cmd
|
|
||||||
msg, err := c.socket.RecvMsg(&cmd)
|
|
||||||
if err != nil {
|
|
||||||
c.socketError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.recvCh <- recvCmd{
|
|
||||||
Cmd: cmd,
|
|
||||||
Msg: msg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerServer) socketError(err error) {
|
|
||||||
c.doneOnce.Do(func() {
|
|
||||||
c.err = err
|
|
||||||
close(c.done)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerServer) waitLoop() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case pid := <-c.waitPid:
|
|
||||||
var waitStatus syscall.WaitStatus
|
|
||||||
var rusage syscall.Rusage
|
|
||||||
|
|
||||||
_, err := syscall.Wait4(pid, &waitStatus, 0, &rusage)
|
|
||||||
for err == syscall.EINTR {
|
|
||||||
_, err = syscall.Wait4(pid, &waitStatus, 0, &rusage)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.waitPidResult <- waitPidResult{
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.waitPidResult <- waitPidResult{
|
|
||||||
WaitStatus: waitStatus,
|
|
||||||
Rusage: rusage,
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-c.waitAll:
|
|
||||||
for {
|
|
||||||
if _, err := syscall.Wait4(-1, nil, syscall.WNOHANG, nil); err != nil && err != syscall.EINTR {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.waitAllDone <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerServer) serve() error {
|
func (c *containerServer) serve() error {
|
||||||
for {
|
for {
|
||||||
cmd, msg, err := c.recvCmd()
|
cmd, msg, err := c.recvCmd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("serve: recvCmd: %w", err)
|
return fmt.Errorf("serve: recvCmd %v", err)
|
||||||
}
|
}
|
||||||
if err := c.handleCmd(cmd, msg); err != nil {
|
if err := c.handleCmd(cmd, msg); err != nil {
|
||||||
return fmt.Errorf("serve: failed to execute cmd: %w", err)
|
return fmt.Errorf("serve: failed to execute cmd %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerServer) handleCmd(cmd cmd, msg unixsocket.Msg) error {
|
func (c *containerServer) handleCmd(cmd *cmd, msg *unixsocket.Msg) error {
|
||||||
switch cmd.Cmd {
|
switch cmd.Cmd {
|
||||||
case cmdPing:
|
case cmdPing:
|
||||||
return c.handlePing()
|
return c.handlePing()
|
||||||
@ -225,154 +89,5 @@ func (c *containerServer) handleCmd(cmd cmd, msg unixsocket.Msg) error {
|
|||||||
case cmdExecve:
|
case cmdExecve:
|
||||||
return c.handleExecve(cmd.ExecCmd, msg)
|
return c.handleExecve(cmd.ExecCmd, msg)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unknown command: %v", cmd.Cmd)
|
return fmt.Errorf("unknown command: %s", cmd.Cmd)
|
||||||
}
|
|
||||||
|
|
||||||
func initContainer(c containerConfig) error {
|
|
||||||
if err := initFileSystem(c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := syscall.Setdomainname([]byte(c.DomainName)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := syscall.Sethostname([]byte(c.HostName)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.Chdir(c.WorkDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(c.InitCommand) > 0 {
|
|
||||||
cm := exec.Command(c.InitCommand[0], c.InitCommand[1:]...)
|
|
||||||
if output, err := cm.CombinedOutput(); err != nil {
|
|
||||||
os.Stderr.Write(output)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFileSystem(c containerConfig) error {
|
|
||||||
// mount tmpfs as root
|
|
||||||
const tmpfs = "tmpfs"
|
|
||||||
if err := syscall.Mount(tmpfs, c.ContainerRoot, tmpfs, 0, ""); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: mount /: %w", err)
|
|
||||||
}
|
|
||||||
// change dir to container root
|
|
||||||
if err := syscall.Chdir(c.ContainerRoot); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: chdir: %w", err)
|
|
||||||
}
|
|
||||||
// performing mounts
|
|
||||||
for _, m := range c.Mounts {
|
|
||||||
if err := m.Mount(); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: mount %v: %w", m, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// pivot root
|
|
||||||
const oldRoot = "old_root"
|
|
||||||
if err := os.Mkdir(oldRoot, 0755); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: mkdir old_root: %w", err)
|
|
||||||
}
|
|
||||||
if err := syscall.PivotRoot(c.ContainerRoot, oldRoot); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: pivot_root(%s, %s): %w", c.ContainerRoot, oldRoot, err)
|
|
||||||
}
|
|
||||||
if err := syscall.Unmount(oldRoot, syscall.MNT_DETACH); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: unmount old_root: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.Remove(oldRoot); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: unlink old_root: %w", err)
|
|
||||||
}
|
|
||||||
// create symlinks
|
|
||||||
for _, l := range c.SymbolicLinks {
|
|
||||||
// ensure dir exists
|
|
||||||
dir := filepath.Dir(l.LinkPath)
|
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: mkdir_all(%s): %w", dir, err)
|
|
||||||
}
|
|
||||||
if err := os.Symlink(l.Target, l.LinkPath); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: symlink: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// mask paths
|
|
||||||
for _, p := range c.MaskPaths {
|
|
||||||
if err := maskPath(p); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: mask path: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// readonly root
|
|
||||||
const remountFlag = syscall.MS_BIND | syscall.MS_REMOUNT | syscall.MS_RDONLY | syscall.MS_NOATIME | syscall.MS_NOSUID
|
|
||||||
if err := syscall.Mount(tmpfs, "/", tmpfs, remountFlag, ""); err != nil {
|
|
||||||
return fmt.Errorf("init_fs: readonly remount /: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerServer) recvCmd() (cmd, unixsocket.Msg, error) {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return cmd{}, unixsocket.Msg{}, c.err
|
|
||||||
|
|
||||||
case recv := <-c.recvCh:
|
|
||||||
return recv.Cmd, recv.Msg, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerServer) sendReplyFiles(rep reply, msg unixsocket.Msg, fileToClose []*os.File) error {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return c.err
|
|
||||||
|
|
||||||
case c.sendCh <- sendReply{Reply: rep, Msg: msg, FileToClose: fileToClose}:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerServer) sendReply(rep reply, msg unixsocket.Msg) error {
|
|
||||||
return c.sendReplyFiles(rep, msg, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendErrorReply sends error reply
|
|
||||||
func (c *containerServer) sendErrorReply(ft string, v ...interface{}) error {
|
|
||||||
errRep := &errorReply{
|
|
||||||
Msg: fmt.Sprintf(ft, v...),
|
|
||||||
}
|
|
||||||
// store errno
|
|
||||||
if len(v) == 1 {
|
|
||||||
if errno, ok := v[0].(syscall.Errno); ok {
|
|
||||||
errRep.Errno = &errno
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.sendReply(reply{Error: errRep}, unixsocket.Msg{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func closeOnExecAllFds() error {
|
|
||||||
// get all fd from /proc/self/fd
|
|
||||||
const fdPath = "/proc/self/fd"
|
|
||||||
fds, err := os.ReadDir(fdPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, f := range fds {
|
|
||||||
fd, err := strconv.Atoi(f.Name())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
syscall.CloseOnExec(fd)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskPath(path string) error {
|
|
||||||
// bind mount /dev/null if it is file
|
|
||||||
if err := syscall.Mount("/dev/null", path, "", syscall.MS_BIND, ""); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
if errors.Is(err, syscall.ENOTDIR) {
|
|
||||||
// otherwise, mount tmpfs to mask it
|
|
||||||
return syscall.Mount("tmpfs", path, "tmpfs", syscall.MS_RDONLY, "")
|
|
||||||
}
|
|
||||||
return fmt.Errorf("mask path: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ignoreSignals() {
|
|
||||||
signal.Ignore(signalToIgnore...)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,44 +1,36 @@
|
|||||||
// Package container provides pre-forked container environment to
|
// Package container provides pre-forked container environment to
|
||||||
// run programs in isolated Linux namespaces.
|
// run programs in isolated Linux namespaces.
|
||||||
//
|
//
|
||||||
// # Overview
|
// Overview
|
||||||
//
|
//
|
||||||
// It creates container within unshared container and communicate
|
// It creates container within unshared container and communicate
|
||||||
// with host process using unix socket with
|
// with host process using unix socket with
|
||||||
// oob for fd / pid and commands encoded by gob.
|
// oob for fd / pid and commands encoded by gob.
|
||||||
//
|
//
|
||||||
// # Protocol
|
// Protocol
|
||||||
//
|
//
|
||||||
// Host to container communication protocol is single threaded and always initiated by
|
// Host to container communication protocol is single threaded and always initiated by
|
||||||
// the host:
|
// the host:
|
||||||
//
|
//
|
||||||
// ## ping (alive check)
|
// - ping (alive check):
|
||||||
//
|
|
||||||
// - send: ping
|
|
||||||
// - reply: pong
|
// - reply: pong
|
||||||
//
|
//
|
||||||
// ## conf (set configuration)
|
// - conf (set configuration):
|
||||||
//
|
// - reply pong
|
||||||
// - send: conf
|
|
||||||
// - reply:
|
|
||||||
//
|
|
||||||
// ## open (open files in given mode inside container):
|
|
||||||
//
|
//
|
||||||
|
// - open (open files in given mode inside container):
|
||||||
// - send: []OpenCmd
|
// - send: []OpenCmd
|
||||||
// - reply: "success", file fds / "error"
|
// - reply: "success", file fds / "error"
|
||||||
//
|
//
|
||||||
// ## delete (unlink file / rmdir dir inside container):
|
// - delete (unlink file / rmdir dir inside container):
|
||||||
//
|
|
||||||
// - send: path
|
// - send: path
|
||||||
// - reply: "finished" / "error"
|
// - reply: "finished" / "error"
|
||||||
//
|
//
|
||||||
// ## reset (clean up container for later use (clear workdir / tmp)):
|
// - reset (clean up container for later use (clear workdir / tmp)):
|
||||||
//
|
|
||||||
// - send:
|
// - send:
|
||||||
// - reply: "success"
|
// - reply: "success"
|
||||||
//
|
//
|
||||||
// ## execve: (execute file inside container):
|
// - execve: (execute file inside container):
|
||||||
//
|
|
||||||
// - send: argv, env, rLimits, fds
|
// - send: argv, env, rLimits, fds
|
||||||
// - reply:
|
// - reply:
|
||||||
// - success: "success", pid
|
// - success: "success", pid
|
||||||
|
|||||||
@ -3,9 +3,7 @@ package container
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
@ -24,24 +22,11 @@ type Builder struct {
|
|||||||
// Root is container root mount path, empty uses current work path
|
// Root is container root mount path, empty uses current work path
|
||||||
Root string
|
Root string
|
||||||
|
|
||||||
// TmpRoot defines the tmp dir pattern if not nil. Temp directory will be created as container root dir
|
|
||||||
TmpRoot string
|
|
||||||
|
|
||||||
// Mounts defines container mount points, empty uses default mounts
|
// Mounts defines container mount points, empty uses default mounts
|
||||||
Mounts []mount.Mount
|
Mounts []mount.SyscallParams
|
||||||
|
|
||||||
// SymbolicLinks defines symlinks to be created after mount file system
|
|
||||||
SymbolicLinks []SymbolicLink
|
|
||||||
|
|
||||||
// MaskPaths defines paths to be masked to avoid reading information from
|
|
||||||
// outside of the container
|
|
||||||
MaskPaths []string
|
|
||||||
|
|
||||||
// WorkDir defines container default work directory (default: /w)
|
|
||||||
WorkDir string
|
|
||||||
|
|
||||||
// Stderr defines whether to dup container stderr to stderr for debug
|
// Stderr defines whether to dup container stderr to stderr for debug
|
||||||
Stderr io.Writer
|
Stderr bool
|
||||||
|
|
||||||
// ExecFile defines executable that called Init, otherwise defer current
|
// ExecFile defines executable that called Init, otherwise defer current
|
||||||
// executable (/proc/self/exe)
|
// executable (/proc/self/exe)
|
||||||
@ -52,29 +37,6 @@ type Builder struct {
|
|||||||
|
|
||||||
// Clone flags defines unshare clone flag to create container
|
// Clone flags defines unshare clone flag to create container
|
||||||
CloneFlags uintptr
|
CloneFlags uintptr
|
||||||
|
|
||||||
// HostName set container hostname (default: go-sandbox)
|
|
||||||
HostName string
|
|
||||||
|
|
||||||
// DomainName set container domainname (default: go-sandbox)
|
|
||||||
DomainName string
|
|
||||||
|
|
||||||
// InitCommand defines command that runs after the initialization of the container
|
|
||||||
// to do additional setups (for example, loopback network)
|
|
||||||
InitCommand []string
|
|
||||||
|
|
||||||
// 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
|
|
||||||
type SymbolicLink struct {
|
|
||||||
LinkPath string
|
|
||||||
Target string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CredGenerator generates uid / gid credential used by container
|
// CredGenerator generates uid / gid credential used by container
|
||||||
@ -89,141 +51,86 @@ type Environment interface {
|
|||||||
Open([]OpenCmd) ([]*os.File, error)
|
Open([]OpenCmd) ([]*os.File, error)
|
||||||
Delete(p string) error
|
Delete(p string) error
|
||||||
Reset() error
|
Reset() error
|
||||||
Execve(context.Context, ExecveParam) runner.Result
|
Execve(context.Context, ExecveParam) <-chan runner.Result
|
||||||
Destroy() error
|
Destroy() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// container manages single pre-forked container environment
|
// container manages single pre-forked container environment
|
||||||
type container struct {
|
type container struct {
|
||||||
process *os.Process // underlying container init pid
|
pid int // underlying container init pid
|
||||||
socket *socket // host - container communication
|
socket *socket // host - container communication
|
||||||
mu sync.Mutex // lock to avoid race condition
|
mu sync.Mutex // lock to avoid race condition
|
||||||
|
|
||||||
done chan struct{}
|
|
||||||
err error
|
|
||||||
doneOnce sync.Once
|
|
||||||
|
|
||||||
recvCh chan recvReply
|
|
||||||
sendCh chan sendCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
type recvReply struct {
|
|
||||||
Reply reply
|
|
||||||
Msg unixsocket.Msg
|
|
||||||
}
|
|
||||||
|
|
||||||
type sendCmd struct {
|
|
||||||
Cmd cmd
|
|
||||||
Msg unixsocket.Msg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build creates new environment with underlying container
|
// Build creates new environment with underlying container
|
||||||
func (b *Builder) Build() (Environment, error) {
|
func (b *Builder) Build() (Environment, error) {
|
||||||
c, err := b.startContainer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// avoid non cinit enabled executable running as container init process
|
|
||||||
if err = c.Ping(); err != nil {
|
|
||||||
c.Destroy()
|
|
||||||
return nil, fmt.Errorf("container: init not responding to ping: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// container mount points
|
|
||||||
mounts := b.Mounts
|
|
||||||
if len(mounts) == 0 {
|
|
||||||
mounts = mount.NewDefaultBuilder().
|
|
||||||
WithTmpfs("w", ""). // work dir
|
|
||||||
WithTmpfs("tmp", ""). // tmp
|
|
||||||
FilterNotExist().Mounts
|
|
||||||
}
|
|
||||||
|
|
||||||
// container symbolic links
|
|
||||||
links := b.SymbolicLinks
|
|
||||||
if len(links) == 0 {
|
|
||||||
links = defaultSymLinks
|
|
||||||
}
|
|
||||||
|
|
||||||
maskPaths := b.MaskPaths
|
|
||||||
if len(maskPaths) == 0 {
|
|
||||||
maskPaths = defaultMaskPaths
|
|
||||||
}
|
|
||||||
|
|
||||||
// container root directory on the host
|
|
||||||
root := b.Root
|
|
||||||
if b.TmpRoot != "" {
|
|
||||||
if root, err = os.MkdirTemp(b.Root, b.TmpRoot); err != nil {
|
|
||||||
return nil, fmt.Errorf("container: failed to make tmp container root at %s: %w", b.Root, err)
|
|
||||||
}
|
|
||||||
defer os.Remove(root)
|
|
||||||
}
|
|
||||||
if root == "" {
|
|
||||||
if root, err = os.Getwd(); err != nil {
|
|
||||||
return nil, fmt.Errorf("container: failed to get work directory: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
workDir := containerWD
|
|
||||||
if b.WorkDir != "" {
|
|
||||||
workDir = b.WorkDir
|
|
||||||
}
|
|
||||||
hostName := containerName
|
|
||||||
if b.HostName != "" {
|
|
||||||
hostName = b.HostName
|
|
||||||
}
|
|
||||||
domainName := containerName
|
|
||||||
if b.DomainName != "" {
|
|
||||||
domainName = b.DomainName
|
|
||||||
}
|
|
||||||
|
|
||||||
// set configuration and check if container creation successful
|
|
||||||
if err = c.conf(&containerConfig{
|
|
||||||
WorkDir: workDir,
|
|
||||||
HostName: hostName,
|
|
||||||
DomainName: domainName,
|
|
||||||
ContainerRoot: root,
|
|
||||||
Mounts: mounts,
|
|
||||||
SymbolicLinks: links,
|
|
||||||
MaskPaths: maskPaths,
|
|
||||||
InitCommand: b.InitCommand,
|
|
||||||
Cred: b.CredGenerator != nil,
|
|
||||||
ContainerUID: b.ContainerUID,
|
|
||||||
ContainerGID: b.ContainerGID,
|
|
||||||
UnshareCgroup: b.UnshareCgroupBeforeExec,
|
|
||||||
}); err != nil {
|
|
||||||
c.Destroy()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) startContainer() (*container, error) {
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
cred syscall.Credential
|
cred syscall.Credential
|
||||||
uidMap, gidMap []syscall.SysProcIDMap
|
uidMap, gidMap []syscall.SysProcIDMap
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// container mount points
|
||||||
|
mounts := b.Mounts
|
||||||
|
if len(mounts) == 0 {
|
||||||
|
if mounts, err = mount.NewDefaultBuilder().
|
||||||
|
WithTmpfs("w", ""). // work dir
|
||||||
|
WithTmpfs("tmp", ""). // tmp
|
||||||
|
Build(true); err != nil {
|
||||||
|
return nil, fmt.Errorf("container: failed to build rootfs mount %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// container root directory on the host
|
||||||
|
root := b.Root
|
||||||
|
if root == "" {
|
||||||
|
if root, err = os.Getwd(); err != nil {
|
||||||
|
return nil, fmt.Errorf("container: failed to get work directory %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare stdin / stdout / stderr
|
||||||
|
devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("container: failed to open devNull %v", err)
|
||||||
|
}
|
||||||
|
defer devNull.Close()
|
||||||
|
|
||||||
|
files := make([]uintptr, 0, 4)
|
||||||
|
files = append(files, devNull.Fd(), devNull.Fd())
|
||||||
|
if b.Stderr {
|
||||||
|
files = append(files, os.Stderr.Fd())
|
||||||
|
} else {
|
||||||
|
files = append(files, devNull.Fd())
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare container exec file
|
||||||
|
execFile, err := b.exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("container: prepare exec %v", err)
|
||||||
|
}
|
||||||
|
defer execFile.Close()
|
||||||
|
|
||||||
// prepare host <-> container unix socket
|
// prepare host <-> container unix socket
|
||||||
ins, outs, err := newPassCredSocketPair()
|
ins, outs, err := newPassCredSocketPair()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("container: failed to create socket: %w", err)
|
return nil, fmt.Errorf("container: failed to create socket: %v", err)
|
||||||
}
|
}
|
||||||
defer outs.Close()
|
defer outs.Close()
|
||||||
|
|
||||||
outf, err := outs.File()
|
outf, err := outs.File()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ins.Close()
|
ins.Close()
|
||||||
return nil, fmt.Errorf("container: failed to dup container socket fd: %w", err)
|
return nil, fmt.Errorf("container: failed to dup container socket fd %v", err)
|
||||||
}
|
}
|
||||||
defer outf.Close()
|
defer outf.Close()
|
||||||
|
|
||||||
|
files = append(files, uintptr(outf.Fd()))
|
||||||
|
|
||||||
// prepare container running credential
|
// prepare container running credential
|
||||||
if b.CredGenerator != nil {
|
if b.CredGenerator != nil {
|
||||||
cred = b.CredGenerator.Get()
|
cred = b.CredGenerator.Get()
|
||||||
uidMap, gidMap = b.getIDMapping(&cred)
|
uidMap, gidMap = getIDMapping(&cred)
|
||||||
} else {
|
|
||||||
uidMap = []syscall.SysProcIDMap{{HostID: os.Geteuid(), Size: 1}}
|
|
||||||
gidMap = []syscall.SysProcIDMap{{HostID: os.Getegid(), Size: 1}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cloneFlag uintptr
|
var cloneFlag uintptr
|
||||||
@ -233,86 +140,42 @@ func (b *Builder) startContainer() (*container, error) {
|
|||||||
cloneFlag = b.CloneFlags & forkexec.UnshareFlags
|
cloneFlag = b.CloneFlags & forkexec.UnshareFlags
|
||||||
}
|
}
|
||||||
|
|
||||||
exe := "/proc/self/exe"
|
r := &forkexec.Runner{
|
||||||
if b.ExecFile != "" {
|
Args: []string{os.Args[0], initArg},
|
||||||
exe = b.ExecFile
|
|
||||||
}
|
|
||||||
args := []string{exe, initArg}
|
|
||||||
|
|
||||||
r := exec.Cmd{
|
|
||||||
Path: exe,
|
|
||||||
Args: args,
|
|
||||||
Env: []string{PathEnv},
|
Env: []string{PathEnv},
|
||||||
Stderr: b.Stderr,
|
ExecFile: execFile.Fd(),
|
||||||
ExtraFiles: []*os.File{outf},
|
Files: files,
|
||||||
SysProcAttr: &syscall.SysProcAttr{
|
WorkDir: containerWD,
|
||||||
Cloneflags: cloneFlag,
|
CloneFlags: cloneFlag,
|
||||||
UidMappings: uidMap,
|
Mounts: mounts,
|
||||||
GidMappings: gidMap,
|
HostName: containerName,
|
||||||
AmbientCaps: []uintptr{
|
DomainName: containerName,
|
||||||
unix.CAP_SYS_ADMIN,
|
PivotRoot: root,
|
||||||
unix.CAP_SYS_RESOURCE,
|
UIDMappings: uidMap,
|
||||||
},
|
GIDMappings: gidMap,
|
||||||
Pdeathsig: syscall.SIGKILL,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
if err = r.Start(); err != nil {
|
pid, err := r.Start()
|
||||||
|
if err != nil {
|
||||||
ins.Close()
|
ins.Close()
|
||||||
return nil, fmt.Errorf("container: failed to start container: %w", err)
|
return nil, fmt.Errorf("container: failed to start container %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &container{
|
c := &container{
|
||||||
process: r.Process,
|
pid: pid,
|
||||||
socket: newSocket(ins),
|
socket: newSocket(ins),
|
||||||
recvCh: make(chan recvReply, 1),
|
|
||||||
sendCh: make(chan sendCmd, 1),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
}
|
||||||
go c.sendLoop()
|
|
||||||
go c.recvLoop()
|
// set configuration and check if container creation successful
|
||||||
|
if err = c.conf(&containerConfig{
|
||||||
|
Cred: b.CredGenerator != nil,
|
||||||
|
}); err != nil {
|
||||||
|
c.Destroy()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *container) sendLoop() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return
|
|
||||||
|
|
||||||
case cmd, ok := <-c.sendCh:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := c.socket.SendMsg(cmd.Cmd, cmd.Msg); err != nil {
|
|
||||||
c.socketError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *container) recvLoop() {
|
|
||||||
for {
|
|
||||||
var reply reply
|
|
||||||
msg, err := c.socket.RecvMsg(&reply)
|
|
||||||
if err != nil {
|
|
||||||
c.socketError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.recvCh <- recvReply{
|
|
||||||
Reply: reply,
|
|
||||||
Msg: msg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *container) socketError(err error) {
|
|
||||||
c.doneOnce.Do(func() {
|
|
||||||
c.err = err
|
|
||||||
close(c.done)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy kill the container process (with its children)
|
// Destroy kill the container process (with its children)
|
||||||
// if stderr enabled, collect the output as error
|
// if stderr enabled, collect the output as error
|
||||||
func (c *container) Destroy() error {
|
func (c *container) Destroy() error {
|
||||||
@ -324,11 +187,29 @@ func (c *container) Destroy() error {
|
|||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
// kill process
|
// kill process
|
||||||
c.process.Kill()
|
var wstatus unix.WaitStatus
|
||||||
_, err := c.process.Wait()
|
unix.Kill(c.pid, unix.SIGKILL)
|
||||||
|
// wait for container process to exit
|
||||||
|
_, err := unix.Wait4(c.pid, &wstatus, 0, nil)
|
||||||
|
for err == unix.EINTR {
|
||||||
|
_, err = unix.Wait4(c.pid, &wstatus, 0, nil)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exec prepares executable
|
||||||
|
func (b *Builder) exec() (*os.File, error) {
|
||||||
|
if b.ExecFile != "" {
|
||||||
|
return os.Open(b.ExecFile)
|
||||||
|
}
|
||||||
|
return OpenCurrentExec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenCurrentExec opens current executable (/proc/self/exe)
|
||||||
|
func OpenCurrentExec() (*os.File, error) {
|
||||||
|
return os.Open(currentExec)
|
||||||
|
}
|
||||||
|
|
||||||
// newPassCredSocketPair creates socket pair and let the first socket to receive credential information
|
// newPassCredSocketPair creates socket pair and let the first socket to receive credential information
|
||||||
func newPassCredSocketPair() (*unixsocket.Socket, *unixsocket.Socket, error) {
|
func newPassCredSocketPair() (*unixsocket.Socket, *unixsocket.Socket, error) {
|
||||||
ins, outs, err := unixsocket.NewSocketPair()
|
ins, outs, err := unixsocket.NewSocketPair()
|
||||||
@ -343,17 +224,7 @@ func newPassCredSocketPair() (*unixsocket.Socket, *unixsocket.Socket, error) {
|
|||||||
return ins, outs, nil
|
return ins, outs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) getIDMapping(cred *syscall.Credential) ([]syscall.SysProcIDMap, []syscall.SysProcIDMap) {
|
func getIDMapping(cred *syscall.Credential) ([]syscall.SysProcIDMap, []syscall.SysProcIDMap) {
|
||||||
cUID := b.ContainerUID
|
|
||||||
if cUID == 0 {
|
|
||||||
cUID = containerUID
|
|
||||||
}
|
|
||||||
|
|
||||||
cGID := b.ContainerGID
|
|
||||||
if cGID == 0 {
|
|
||||||
cGID = containerGID
|
|
||||||
}
|
|
||||||
|
|
||||||
uidMap := []syscall.SysProcIDMap{
|
uidMap := []syscall.SysProcIDMap{
|
||||||
{
|
{
|
||||||
ContainerID: 0,
|
ContainerID: 0,
|
||||||
@ -361,7 +232,7 @@ func (b *Builder) getIDMapping(cred *syscall.Credential) ([]syscall.SysProcIDMap
|
|||||||
Size: 1,
|
Size: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ContainerID: cUID,
|
ContainerID: containerUID,
|
||||||
HostID: int(cred.Uid),
|
HostID: int(cred.Uid),
|
||||||
Size: 1,
|
Size: 1,
|
||||||
},
|
},
|
||||||
@ -374,7 +245,7 @@ func (b *Builder) getIDMapping(cred *syscall.Credential) ([]syscall.SysProcIDMap
|
|||||||
Size: 1,
|
Size: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ContainerID: cGID,
|
ContainerID: containerGID,
|
||||||
HostID: int(cred.Gid),
|
HostID: int(cred.Gid),
|
||||||
Size: 1,
|
Size: 1,
|
||||||
},
|
},
|
||||||
@ -382,33 +253,3 @@ func (b *Builder) getIDMapping(cred *syscall.Credential) ([]syscall.SysProcIDMap
|
|||||||
|
|
||||||
return uidMap, gidMap
|
return uidMap, gidMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *container) recvAckReply(name string) error {
|
|
||||||
reply, _, err := c.recvReply()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: recv ack: %w", name, err)
|
|
||||||
}
|
|
||||||
if reply.Error != nil {
|
|
||||||
return fmt.Errorf("%s: container error: %v", name, reply.Error)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (c *container) recvReply() (reply, unixsocket.Msg, error) {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return reply{}, unixsocket.Msg{}, c.err
|
|
||||||
|
|
||||||
case recv := <-c.recvCh:
|
|
||||||
return recv.Reply, recv.Msg, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *container) sendCmd(cmd cmd, msg unixsocket.Msg) error {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return c.err
|
|
||||||
|
|
||||||
case c.sendCh <- sendCmd{Cmd: cmd, Msg: msg}:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,13 +3,12 @@ package container
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ping send ping message to container, wait for 3 second before timeout
|
// Ping send ping message to container
|
||||||
func (c *container) Ping() error {
|
func (c *container) Ping() error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
@ -23,8 +22,8 @@ func (c *container) Ping() error {
|
|||||||
cmd := cmd{
|
cmd := cmd{
|
||||||
Cmd: cmdPing,
|
Cmd: cmdPing,
|
||||||
}
|
}
|
||||||
if err := c.sendCmd(cmd, unixsocket.Msg{}); err != nil {
|
if err := c.sendCmd(&cmd, nil); err != nil {
|
||||||
return fmt.Errorf("ping: %w", err)
|
return fmt.Errorf("ping: %v", err)
|
||||||
}
|
}
|
||||||
// receive no error
|
// receive no error
|
||||||
return c.recvAckReply("ping")
|
return c.recvAckReply("ping")
|
||||||
@ -39,8 +38,8 @@ func (c *container) conf(conf *containerConfig) error {
|
|||||||
Cmd: cmdConf,
|
Cmd: cmdConf,
|
||||||
ConfCmd: &confCmd{Conf: *conf},
|
ConfCmd: &confCmd{Conf: *conf},
|
||||||
}
|
}
|
||||||
if err := c.sendCmd(cmd, unixsocket.Msg{}); err != nil {
|
if err := c.sendCmd(&cmd, nil); err != nil {
|
||||||
return fmt.Errorf("conf: %w", err)
|
return fmt.Errorf("conf: %v", err)
|
||||||
}
|
}
|
||||||
return c.recvAckReply("conf")
|
return c.recvAckReply("conf")
|
||||||
}
|
}
|
||||||
@ -50,36 +49,32 @@ func (c *container) Open(p []OpenCmd) ([]*os.File, error) {
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
syscall.ForkLock.RLock()
|
|
||||||
defer syscall.ForkLock.RUnlock()
|
|
||||||
|
|
||||||
// send copyin
|
// send copyin
|
||||||
cmd := cmd{
|
cmd := cmd{
|
||||||
Cmd: cmdOpen,
|
Cmd: cmdOpen,
|
||||||
OpenCmd: p,
|
OpenCmd: p,
|
||||||
}
|
}
|
||||||
if err := c.sendCmd(cmd, unixsocket.Msg{}); err != nil {
|
if err := c.sendCmd(&cmd, nil); err != nil {
|
||||||
return nil, fmt.Errorf("open: %w", err)
|
return nil, fmt.Errorf("open: %v", err)
|
||||||
}
|
}
|
||||||
reply, msg, err := c.recvReply()
|
reply, msg, err := c.recvReply()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open: %w", err)
|
return nil, fmt.Errorf("open: %v", err)
|
||||||
}
|
}
|
||||||
if reply.Error != nil {
|
if reply.Error != nil {
|
||||||
return nil, fmt.Errorf("open: %v", reply.Error)
|
return nil, fmt.Errorf("open: %v", reply.Error)
|
||||||
}
|
}
|
||||||
if len(msg.Fds) != len(p) {
|
if len(msg.Fds) != len(p) {
|
||||||
closeFds(msg.Fds)
|
closeFds(msg.Fds)
|
||||||
return nil, fmt.Errorf("open: unexpected number of fds: got %d, want %d", len(msg.Fds), len(p))
|
return nil, fmt.Errorf("open: unexpected number of fd %v / %v", len(msg.Fds), len(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := make([]*os.File, 0, len(p))
|
ret := make([]*os.File, 0, len(p))
|
||||||
for i, fd := range msg.Fds {
|
for i, fd := range msg.Fds {
|
||||||
syscall.CloseOnExec(fd)
|
|
||||||
f := os.NewFile(uintptr(fd), p[i].Path)
|
f := os.NewFile(uintptr(fd), p[i].Path)
|
||||||
if f == nil {
|
if f == nil {
|
||||||
closeFds(msg.Fds)
|
closeFds(msg.Fds)
|
||||||
return nil, fmt.Errorf("open: failed to create file for fd: %d", fd)
|
return nil, fmt.Errorf("open: failed NewFile %v", fd)
|
||||||
}
|
}
|
||||||
ret = append(ret, f)
|
ret = append(ret, f)
|
||||||
}
|
}
|
||||||
@ -95,8 +90,8 @@ func (c *container) Delete(p string) error {
|
|||||||
Cmd: cmdDelete,
|
Cmd: cmdDelete,
|
||||||
DeleteCmd: &deleteCmd{Path: p},
|
DeleteCmd: &deleteCmd{Path: p},
|
||||||
}
|
}
|
||||||
if err := c.sendCmd(cmd, unixsocket.Msg{}); err != nil {
|
if err := c.sendCmd(&cmd, nil); err != nil {
|
||||||
return fmt.Errorf("delete: %w", err)
|
return fmt.Errorf("delete: %v", err)
|
||||||
}
|
}
|
||||||
return c.recvAckReply("delete")
|
return c.recvAckReply("delete")
|
||||||
}
|
}
|
||||||
@ -109,8 +104,32 @@ func (c *container) Reset() error {
|
|||||||
cmd := cmd{
|
cmd := cmd{
|
||||||
Cmd: cmdReset,
|
Cmd: cmdReset,
|
||||||
}
|
}
|
||||||
if err := c.sendCmd(cmd, unixsocket.Msg{}); err != nil {
|
if err := c.sendCmd(&cmd, nil); err != nil {
|
||||||
return fmt.Errorf("reset: %w", err)
|
return fmt.Errorf("reset: %v", err)
|
||||||
}
|
}
|
||||||
return c.recvAckReply("reset")
|
return c.recvAckReply("reset")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *container) recvAckReply(name string) error {
|
||||||
|
reply, _, err := c.recvReply()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%v: recvAck %v", name, err)
|
||||||
|
}
|
||||||
|
if reply.Error != nil {
|
||||||
|
return fmt.Errorf("%v: container error %v", name, reply.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *container) recvReply() (*reply, *unixsocket.Msg, error) {
|
||||||
|
reply := new(reply)
|
||||||
|
msg, err := c.socket.RecvMsg(reply)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return reply, msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *container) sendCmd(cmd *cmd, msg *unixsocket.Msg) error {
|
||||||
|
return c.socket.SendMsg(cmd, msg)
|
||||||
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/rlimit"
|
"github.com/criyle/go-sandbox/pkg/rlimit"
|
||||||
"github.com/criyle/go-sandbox/pkg/seccomp"
|
|
||||||
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
||||||
"github.com/criyle/go-sandbox/runner"
|
"github.com/criyle/go-sandbox/runner"
|
||||||
)
|
)
|
||||||
@ -25,152 +24,142 @@ type ExecveParam struct {
|
|||||||
// ExecFile specifies file descriptor for executable file using fexecve
|
// ExecFile specifies file descriptor for executable file using fexecve
|
||||||
ExecFile uintptr
|
ExecFile uintptr
|
||||||
|
|
||||||
// CgroupFD specifies file descriptor for cgroup V2
|
|
||||||
CgroupFD uintptr
|
|
||||||
|
|
||||||
// RLimits specifies POSIX Resource limit through setrlimit
|
// RLimits specifies POSIX Resource limit through setrlimit
|
||||||
RLimits []rlimit.RLimit
|
RLimits []rlimit.RLimit
|
||||||
|
|
||||||
// Seccomp specifies seccomp filter
|
|
||||||
Seccomp seccomp.Filter
|
|
||||||
|
|
||||||
// CTTY specifies whether to set controlling TTY
|
|
||||||
CTTY bool
|
|
||||||
|
|
||||||
// SyncFunc calls with pid just before execve (for attach the process to cgroups)
|
// SyncFunc calls with pid just before execve (for attach the process to cgroups)
|
||||||
SyncFunc func(pid int) error
|
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 cancellation as time limit exceeded.
|
// Execve runs process inside container. It accepts context cancelation as time limit exceeded.
|
||||||
func (c *container) Execve(ctx context.Context, param ExecveParam) runner.Result {
|
func (c *container) Execve(ctx context.Context, param ExecveParam) <-chan runner.Result {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
sTime := time.Now()
|
sTime := time.Now()
|
||||||
|
|
||||||
|
// make sure goroutine not leaked (blocked) even if result is not consumed
|
||||||
|
result := make(chan runner.Result, 1)
|
||||||
|
|
||||||
|
errResult := func(f string, v ...interface{}) <-chan runner.Result {
|
||||||
|
result <- runner.Result{
|
||||||
|
Status: runner.StatusRunnerError,
|
||||||
|
Error: fmt.Sprintf(f, v...),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// if execve with fd, put fd at the first parameter
|
// if execve with fd, put fd at the first parameter
|
||||||
var files []int
|
var files []int
|
||||||
if param.ExecFile > 0 {
|
if param.ExecFile > 0 {
|
||||||
files = append(files, int(param.ExecFile))
|
files = append(files, int(param.ExecFile))
|
||||||
}
|
}
|
||||||
if param.CgroupFD > 0 {
|
|
||||||
files = append(files, int(param.CgroupFD))
|
|
||||||
}
|
|
||||||
files = append(files, uintptrSliceToInt(param.Files)...)
|
files = append(files, uintptrSliceToInt(param.Files)...)
|
||||||
msg := unixsocket.Msg{
|
msg := &unixsocket.Msg{
|
||||||
Fds: files,
|
Fds: files,
|
||||||
}
|
}
|
||||||
execCmd := &execCmd{
|
execCmd := &execCmd{
|
||||||
Argv: param.Args,
|
Argv: param.Args,
|
||||||
Env: param.Env,
|
Env: param.Env,
|
||||||
RLimits: param.RLimits,
|
RLimits: param.RLimits,
|
||||||
Seccomp: param.Seccomp,
|
|
||||||
FdExec: param.ExecFile > 0,
|
FdExec: param.ExecFile > 0,
|
||||||
CTTY: param.CTTY,
|
|
||||||
SyncAfter: param.SyncAfterExec,
|
|
||||||
FdCgroup: param.CgroupFD > 0,
|
|
||||||
}
|
}
|
||||||
cm := cmd{
|
cm := cmd{
|
||||||
Cmd: cmdExecve,
|
Cmd: cmdExecve,
|
||||||
ExecCmd: execCmd,
|
ExecCmd: execCmd,
|
||||||
}
|
}
|
||||||
if err := c.sendCmd(cm, msg); err != nil {
|
if err := c.sendCmd(&cm, msg); err != nil {
|
||||||
|
c.mu.Unlock()
|
||||||
return errResult("execve: sendCmd %v", err)
|
return errResult("execve: sendCmd %v", err)
|
||||||
}
|
}
|
||||||
// sync function
|
// sync function
|
||||||
rep, msg, err := c.recvReply()
|
reply, msg, err := c.recvReply()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
c.mu.Unlock()
|
||||||
return errResult("execve: recvReply %v", err)
|
return errResult("execve: recvReply %v", err)
|
||||||
}
|
}
|
||||||
// if sync function did not involved
|
// if sync function did not involved
|
||||||
if rep.Error != nil {
|
if reply.Error != nil || msg == nil || msg.Cred == nil {
|
||||||
return errResult("execve: %v", rep.Error)
|
|
||||||
}
|
|
||||||
// if pid not received
|
|
||||||
if msg.Cred == nil {
|
|
||||||
// tell kill function to exit and sync
|
// tell kill function to exit and sync
|
||||||
c.execveSyncKill()
|
c.execveSyncKill()
|
||||||
// tell err exec function to exit and sync
|
c.mu.Unlock()
|
||||||
c.execveSyncKill()
|
return errResult("execve: no pid received or error %v", reply.Error)
|
||||||
return errResult("execve: no pid received")
|
|
||||||
}
|
}
|
||||||
if param.SyncFunc != nil {
|
if param.SyncFunc != nil {
|
||||||
if err := param.SyncFunc(int(msg.Cred.Pid)); err != nil {
|
if err := param.SyncFunc(int(msg.Cred.Pid)); err != nil {
|
||||||
// tell sync function to exit and recv error
|
// tell sync function to exit and recv error
|
||||||
c.execveSyncKill()
|
c.execveSyncKill()
|
||||||
|
// tell kill function to exit and sync
|
||||||
|
c.execveSyncKill()
|
||||||
|
c.mu.Unlock()
|
||||||
return errResult("execve: syncfunc failed %v", err)
|
return errResult("execve: syncfunc failed %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// send to syncFunc ack ok
|
// send to syncFunc ack ok
|
||||||
if err := c.sendCmd(cmd{Cmd: cmdOk}, unixsocket.Msg{}); err != nil {
|
if err := c.sendCmd(&cmd{Cmd: cmdOk}, nil); err != nil {
|
||||||
|
c.mu.Unlock()
|
||||||
return errResult("execve: ack failed %v", err)
|
return errResult("execve: ack failed %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for done
|
|
||||||
return c.waitForDone(ctx, sTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *container) waitForDone(ctx context.Context, sTime time.Time) runner.Result {
|
|
||||||
mTime := time.Now()
|
mTime := time.Now()
|
||||||
select {
|
|
||||||
case <-c.done: // socket error
|
|
||||||
return convertReplyResult(reply{}, sTime, mTime, c.err)
|
|
||||||
|
|
||||||
case <-ctx.Done(): // cancel
|
waitDone := make(chan struct{})
|
||||||
c.sendCmd(cmd{Cmd: cmdKill}, unixsocket.Msg{}) // kill
|
|
||||||
reply, _, err := c.recvReply()
|
|
||||||
return convertReplyResult(reply, sTime, mTime, err)
|
|
||||||
|
|
||||||
case ret := <-c.recvCh: // result
|
// Wait
|
||||||
err := c.sendCmd(cmd{Cmd: cmdKill}, unixsocket.Msg{}) // kill
|
go func() {
|
||||||
return convertReplyResult(ret.Reply, sTime, mTime, err)
|
reply2, _, err := c.recvReply()
|
||||||
}
|
close(waitDone)
|
||||||
}
|
// done signal (should recv after kill)
|
||||||
|
c.recvReply()
|
||||||
|
// unlock after last read / write
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
func convertReplyResult(reply reply, sTime, mTime time.Time, err error) runner.Result {
|
|
||||||
// handle potential error
|
// handle potential error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return runner.Result{
|
result <- runner.Result{
|
||||||
Status: runner.StatusRunnerError,
|
Status: runner.StatusRunnerError,
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if reply.Error != nil {
|
if reply2.Error != nil {
|
||||||
return runner.Result{
|
result <- runner.Result{
|
||||||
Status: runner.StatusRunnerError,
|
Status: runner.StatusRunnerError,
|
||||||
Error: reply.Error.Error(),
|
Error: reply2.Error.Error(),
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if reply.ExecReply == nil {
|
if reply2.ExecReply == nil {
|
||||||
return runner.Result{
|
result <- runner.Result{
|
||||||
Status: runner.StatusRunnerError,
|
Status: runner.StatusRunnerError,
|
||||||
Error: "execve: no reply received",
|
Error: "execve: no reply received",
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// emit result after all communication finish
|
// emit result after all communication finish
|
||||||
return runner.Result{
|
result <- runner.Result{
|
||||||
Status: reply.ExecReply.Status,
|
Status: reply2.ExecReply.Status,
|
||||||
ExitStatus: reply.ExecReply.ExitStatus,
|
ExitStatus: reply2.ExecReply.ExitStatus,
|
||||||
Time: reply.ExecReply.Time,
|
Time: reply2.ExecReply.Time,
|
||||||
Memory: reply.ExecReply.Memory,
|
Memory: reply2.ExecReply.Memory,
|
||||||
SetUpTime: mTime.Sub(sTime),
|
SetUpTime: mTime.Sub(sTime),
|
||||||
RunningTime: time.Since(mTime),
|
RunningTime: time.Since(mTime),
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Kill (if wait is done, a kill message need to be send to collect zombies)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-waitDone:
|
||||||
|
}
|
||||||
|
c.sendCmd(&cmd{Cmd: cmdKill}, nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// execveSyncKill will send kill and recv reply
|
// execveSyncKill will send kill and recv reply
|
||||||
func (c *container) execveSyncKill() {
|
func (c *container) execveSyncKill() {
|
||||||
c.sendCmd(cmd{Cmd: cmdKill}, unixsocket.Msg{})
|
c.sendCmd(&cmd{Cmd: cmdKill}, nil)
|
||||||
c.recvReply()
|
c.recvReply()
|
||||||
}
|
}
|
||||||
|
|
||||||
func errResult(f string, v ...interface{}) runner.Result {
|
|
||||||
return runner.Result{
|
|
||||||
Status: runner.StatusRunnerError,
|
|
||||||
Error: fmt.Sprintf(f, v...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errNotFound = errors.New("executable file not found in $PATH")
|
|
||||||
errNoPath = errors.New("no PATH environment variable provided for look up")
|
|
||||||
)
|
|
||||||
|
|
||||||
func findExecutable(file string) error {
|
|
||||||
d, err := os.Stat(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fs.ErrPermission
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookPath(name string, env []string) (string, error) {
|
|
||||||
// don't look if abs path provided
|
|
||||||
if filepath.Base(name) != name {
|
|
||||||
return name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't look if exist in current dir
|
|
||||||
if err := findExecutable(name); err == nil {
|
|
||||||
return name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := findPath(env)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
for _, dir := range path {
|
|
||||||
if dir == "" {
|
|
||||||
dir = "."
|
|
||||||
}
|
|
||||||
p := filepath.Join(dir, name)
|
|
||||||
if err := findExecutable(p); err == nil {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func findPath(env []string) ([]string, error) {
|
|
||||||
// find PATH=
|
|
||||||
const pathPrefix = "PATH="
|
|
||||||
for i := len(env) - 1; i >= 0; i-- {
|
|
||||||
s := env[i]
|
|
||||||
if strings.HasPrefix(s, pathPrefix) {
|
|
||||||
return filepath.SplitList(s[len(pathPrefix):]), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errNoPath
|
|
||||||
}
|
|
||||||
@ -5,21 +5,18 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/mount"
|
|
||||||
"github.com/criyle/go-sandbox/pkg/rlimit"
|
"github.com/criyle/go-sandbox/pkg/rlimit"
|
||||||
"github.com/criyle/go-sandbox/pkg/seccomp"
|
|
||||||
"github.com/criyle/go-sandbox/runner"
|
"github.com/criyle/go-sandbox/runner"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmd is the control message send into container
|
// cmd is the control message send into container
|
||||||
type cmd struct {
|
type cmd struct {
|
||||||
|
Cmd string // type of the cmd
|
||||||
|
|
||||||
|
OpenCmd []OpenCmd // open argument
|
||||||
DeleteCmd *deleteCmd // delete argument
|
DeleteCmd *deleteCmd // delete argument
|
||||||
ExecCmd *execCmd // execve argument
|
ExecCmd *execCmd // execve argument
|
||||||
ConfCmd *confCmd // to set configuration
|
ConfCmd *confCmd // to set configuration
|
||||||
|
|
||||||
OpenCmd []OpenCmd // open argument
|
|
||||||
|
|
||||||
Cmd cmdType // type of the cmd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenCmd correspond to a single open syscall
|
// OpenCmd correspond to a single open syscall
|
||||||
@ -39,11 +36,7 @@ type execCmd struct {
|
|||||||
Argv []string // execve argv
|
Argv []string // execve argv
|
||||||
Env []string // execve env
|
Env []string // execve env
|
||||||
RLimits []rlimit.RLimit // execve posix rlimit
|
RLimits []rlimit.RLimit // execve posix rlimit
|
||||||
Seccomp seccomp.Filter // seccomp filter
|
|
||||||
FdExec bool // if use fexecve (fd[0] as exec)
|
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
|
// confCmd stores conf parameter
|
||||||
@ -53,21 +46,7 @@ type confCmd struct {
|
|||||||
|
|
||||||
// ContainerConfig set the container config
|
// ContainerConfig set the container config
|
||||||
type containerConfig struct {
|
type containerConfig struct {
|
||||||
WorkDir string
|
|
||||||
|
|
||||||
HostName string
|
|
||||||
DomainName string
|
|
||||||
|
|
||||||
ContainerRoot string
|
|
||||||
Mounts []mount.Mount
|
|
||||||
SymbolicLinks []SymbolicLink
|
|
||||||
MaskPaths []string
|
|
||||||
InitCommand []string
|
|
||||||
|
|
||||||
ContainerUID int
|
|
||||||
ContainerGID int
|
|
||||||
Cred bool
|
Cred bool
|
||||||
UnshareCgroup bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reply is the reply message send back to controller
|
// reply is the reply message send back to controller
|
||||||
@ -78,8 +57,8 @@ type reply struct {
|
|||||||
|
|
||||||
// errorReply stores error returned back from container
|
// errorReply stores error returned back from container
|
||||||
type errorReply struct {
|
type errorReply struct {
|
||||||
Errno *syscall.Errno
|
|
||||||
Msg string
|
Msg string
|
||||||
|
Errno *syscall.Errno
|
||||||
}
|
}
|
||||||
|
|
||||||
// execReply stores execve result
|
// execReply stores execve result
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
//go:build linux && !mips64 && !mips64le
|
|
||||||
|
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
var signalToIgnore = []os.Signal{
|
|
||||||
// signals that cause run-time panic
|
|
||||||
syscall.SIGBUS, syscall.SIGFPE, syscall.SIGSEGV,
|
|
||||||
// signals that cause the program to exit
|
|
||||||
syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM,
|
|
||||||
// signals that cause the program to exit with a stack dump
|
|
||||||
syscall.SIGQUIT, syscall.SIGILL, syscall.SIGTRAP, syscall.SIGABRT, syscall.SIGSTKFLT, syscall.SIGSYS,
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
//go:build linux && (mips64 || mips64le)
|
|
||||||
|
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
var signalToIgnore = []os.Signal{
|
|
||||||
// signals that cause run-time panic
|
|
||||||
syscall.SIGBUS, syscall.SIGFPE, syscall.SIGSEGV,
|
|
||||||
// signals that cause the program to exit
|
|
||||||
syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM,
|
|
||||||
// signals that cause the program to exit with a stack dump
|
|
||||||
syscall.SIGQUIT, syscall.SIGILL, syscall.SIGTRAP, syscall.SIGABRT, syscall.SIGSYS,
|
|
||||||
}
|
|
||||||
@ -4,66 +4,65 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
"github.com/criyle/go-sandbox/pkg/unixsocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 16k buffer size
|
// 16k buffsize
|
||||||
const bufferSize = 16 << 10
|
const bufferSize = 16 << 10
|
||||||
|
|
||||||
|
var bufferPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return make([]byte, bufferSize)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
type socket struct {
|
type socket struct {
|
||||||
*unixsocket.Socket
|
*unixsocket.Socket
|
||||||
|
|
||||||
buff []byte
|
recvBuff bytes.Buffer
|
||||||
|
|
||||||
decoder *gob.Decoder
|
decoder *gob.Decoder
|
||||||
recvBuff bufferRotator
|
|
||||||
|
|
||||||
encoder *gob.Encoder
|
|
||||||
sendBuff bytes.Buffer
|
sendBuff bytes.Buffer
|
||||||
}
|
encoder *gob.Encoder
|
||||||
|
|
||||||
// bufferRotator replace the underlying Buffers to avoid allocation
|
|
||||||
type bufferRotator struct {
|
|
||||||
*bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bufferRotator) Rotate(buffer *bytes.Buffer) {
|
|
||||||
b.Buffer = buffer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSocket(s *unixsocket.Socket) *socket {
|
func newSocket(s *unixsocket.Socket) *socket {
|
||||||
soc := socket{
|
soc := socket{
|
||||||
Socket: s,
|
Socket: s,
|
||||||
}
|
}
|
||||||
soc.buff = make([]byte, bufferSize)
|
|
||||||
soc.decoder = gob.NewDecoder(&soc.recvBuff)
|
soc.decoder = gob.NewDecoder(&soc.recvBuff)
|
||||||
soc.encoder = gob.NewEncoder(&soc.sendBuff)
|
soc.encoder = gob.NewEncoder(&soc.sendBuff)
|
||||||
|
|
||||||
return &soc
|
return &soc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *socket) RecvMsg(e any) (msg unixsocket.Msg, err error) {
|
func (s *socket) RecvMsg(e interface{}) (*unixsocket.Msg, error) {
|
||||||
n, msg, err := s.Socket.RecvMsg(s.buff)
|
buff := bufferPool.Get().([]byte)
|
||||||
|
defer bufferPool.Put(buff)
|
||||||
|
|
||||||
|
n, msg, err := s.Socket.RecvMsg(buff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return msg, fmt.Errorf("recv msg: %w", err)
|
return nil, fmt.Errorf("RecvMsg: %v", err)
|
||||||
}
|
}
|
||||||
s.recvBuff.Rotate(bytes.NewBuffer(s.buff[:n]))
|
s.recvBuff.Reset()
|
||||||
|
s.recvBuff.Write(buff[:n])
|
||||||
|
|
||||||
if err := s.decoder.Decode(e); err != nil {
|
if err := s.decoder.Decode(e); err != nil {
|
||||||
return msg, fmt.Errorf("recv msg: decode: %w", err)
|
return nil, fmt.Errorf("RecvMsg: failed to decode %v", err)
|
||||||
}
|
}
|
||||||
return msg, nil
|
return msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *socket) SendMsg(e any, msg unixsocket.Msg) error {
|
func (s *socket) SendMsg(e interface{}, msg *unixsocket.Msg) error {
|
||||||
s.sendBuff.Reset()
|
s.sendBuff.Reset()
|
||||||
if err := s.encoder.Encode(e); err != nil {
|
if err := s.encoder.Encode(e); err != nil {
|
||||||
return fmt.Errorf("send msg: encode: %w", err)
|
return fmt.Errorf("SendMsg: failed to encode %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Socket.SendMsg(s.sendBuff.Bytes(), msg); err != nil {
|
if err := s.Socket.SendMsg(s.sendBuff.Bytes(), msg); err != nil {
|
||||||
return fmt.Errorf("send msg: %w", err)
|
return fmt.Errorf("SendMsg: failed to SendMsg %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,10 +54,10 @@ func removeContents(dir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
err1 := os.RemoveAll(filepath.Join(dir, name))
|
err = os.RemoveAll(path.Join(dir, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = err1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
7
go.mod
7
go.mod
@ -1,9 +1,8 @@
|
|||||||
module github.com/criyle/go-sandbox
|
module github.com/criyle/go-sandbox
|
||||||
|
|
||||||
go 1.24
|
go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/elastic/go-seccomp-bpf v1.6.0
|
github.com/seccomp/libseccomp-golang v0.9.1
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c
|
||||||
golang.org/x/sys v0.35.0
|
|
||||||
)
|
)
|
||||||
|
|||||||
18
go.sum
18
go.sum
@ -1,14 +1,4 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46rlXTMfXGWBo=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||||
github.com/elastic/go-seccomp-bpf v1.6.0 h1:NYduiYxRJ0ZkIyQVwlSskcqPPSg6ynu5pK0/d7SQATs=
|
golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c h1:kISX68E8gSkNYAFRFiDU8rl5RIn1sJYKYb/r2vMLDrU=
|
||||||
github.com/elastic/go-seccomp-bpf v1.6.0/go.mod h1:5tFsTvH4NtWGfpjsOQD53H8HdVQ+zSZFRUDSGevC0Kc=
|
golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
package cgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkCgroup(b *testing.B) {
|
|
||||||
if err := EnableV2Nesting(); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
ct, err := GetAvailableControllerV2()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
builder, err := New("benchmark", ct)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
defer builder.Destroy()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
cg, err := builder.New("test")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := cg.SetCPUSet([]byte("0")); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := cg.SetMemoryLimit(4096); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := cg.SetProcLimit(1); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := cg.CPUUsage(); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := cg.MemoryMaxUsage(); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
cg.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCgroupAll(t *testing.T) {
|
|
||||||
// ensure root privilege when testing
|
|
||||||
if os.Getuid() != 0 {
|
|
||||||
t.Skip("no root privilege")
|
|
||||||
}
|
|
||||||
if err := EnableV2Nesting(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
ct, err := GetAvailableControllerV2()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
builder, err := New("benchmark", ct)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer builder.Destroy()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
cg, err := builder.New("test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
cg.Destroy()
|
|
||||||
})
|
|
||||||
if err := cg.SetCPUSet([]byte("0")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := cg.SetMemoryLimit(4096); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := cg.SetProcLimit(1); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := cg.CPUUsage(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := cg.MemoryMaxUsage(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
66
pkg/cgroup/benchmark_test.go
Normal file
66
pkg/cgroup/benchmark_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package cgroup
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func BenchmarkCgroup(b *testing.B) {
|
||||||
|
builder, err := NewBuilder("benchmark").WithCPUAcct().WithMemory().WithPids().FilterByEnv()
|
||||||
|
if err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
cg, err := builder.Build()
|
||||||
|
if err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := cg.SetMemoryLimitInBytes(4096); err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := cg.SetPidsMax(1); err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := cg.CpuacctUsage(); err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := cg.MemoryMaxUsageInBytes(); err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cg.Destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCgroup(t *testing.T) {
|
||||||
|
builder, err := NewBuilder("test").WithCPUAcct().WithMemory().WithPids().FilterByEnv()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cg, err := builder.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := cg.SetMemoryLimitInBytes(4096); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := cg.SetPidsMax(1); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := cg.CpuacctUsage(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := cg.MemoryMaxUsageInBytes(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cg.Destroy()
|
||||||
|
}
|
||||||
68
pkg/cgroup/builder.go
Normal file
68
pkg/cgroup/builder.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package cgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder builds cgroup directories
|
||||||
|
// available: cpuacct, memory, pids
|
||||||
|
type Builder struct {
|
||||||
|
Prefix string
|
||||||
|
CPUAcct, Memory, Pids bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder return a dumb builder without any sub-cgroup
|
||||||
|
func NewBuilder(prefix string) *Builder {
|
||||||
|
return &Builder{
|
||||||
|
Prefix: prefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCPUAcct includes cpuacct cgroup
|
||||||
|
func (b *Builder) WithCPUAcct() *Builder {
|
||||||
|
b.CPUAcct = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMemory includes memory cgroup
|
||||||
|
func (b *Builder) WithMemory() *Builder {
|
||||||
|
b.Memory = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPids includes pids cgroup
|
||||||
|
func (b *Builder) WithPids() *Builder {
|
||||||
|
b.Pids = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterByEnv reads /proc/cgroups and filter out non-exists ones
|
||||||
|
func (b *Builder) FilterByEnv() (*Builder, error) {
|
||||||
|
m, err := GetAllSubCgroup()
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
b.CPUAcct = b.CPUAcct && m["cpuacct"]
|
||||||
|
b.Memory = b.Memory && m["memory"]
|
||||||
|
b.Pids = b.Pids && m["pids"]
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String prints the build properties
|
||||||
|
func (b *Builder) String() string {
|
||||||
|
s := make([]string, 0, 3)
|
||||||
|
for _, t := range []struct {
|
||||||
|
name string
|
||||||
|
enabled bool
|
||||||
|
}{
|
||||||
|
{"cpuacct", b.CPUAcct},
|
||||||
|
{"memory", b.Memory},
|
||||||
|
{"pids", b.Pids},
|
||||||
|
} {
|
||||||
|
if t.enabled {
|
||||||
|
s = append(s, t.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("cgroup builder: [%s]", strings.Join(s, ", "))
|
||||||
|
}
|
||||||
136
pkg/cgroup/cgroup.go
Normal file
136
pkg/cgroup/cgroup.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package cgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cgroup is the combination of sub-cgroups
|
||||||
|
type Cgroup struct {
|
||||||
|
prefix string
|
||||||
|
cpuacct, memory, pids *SubCgroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build creates new cgrouup directories
|
||||||
|
func (b *Builder) Build() (cg *Cgroup, err error) {
|
||||||
|
var (
|
||||||
|
cpuacctPath, memoryPath, pidsPath string
|
||||||
|
)
|
||||||
|
// if failed, remove potential created directory
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
remove(cpuacctPath)
|
||||||
|
remove(memoryPath)
|
||||||
|
remove(pidsPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if b.CPUAcct {
|
||||||
|
if cpuacctPath, err = CreateSubCgroupPath("cpuacct", b.Prefix); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b.Memory {
|
||||||
|
if memoryPath, err = CreateSubCgroupPath("memory", b.Prefix); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b.Pids {
|
||||||
|
if pidsPath, err = CreateSubCgroupPath("pids", b.Prefix); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Cgroup{
|
||||||
|
prefix: b.Prefix,
|
||||||
|
cpuacct: NewSubCgroup(cpuacctPath),
|
||||||
|
memory: NewSubCgroup(memoryPath),
|
||||||
|
pids: NewSubCgroup(pidsPath),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddProc writes cgroup.procs to all sub-cgroup
|
||||||
|
func (c *Cgroup) AddProc(pid int) error {
|
||||||
|
if err := c.cpuacct.WriteUint(cgroupProcs, uint64(pid)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.memory.WriteUint(cgroupProcs, uint64(pid)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.pids.WriteUint(cgroupProcs, uint64(pid)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy removes dir for sub-cgroup, errors are ignored if remove one failed
|
||||||
|
func (c *Cgroup) Destroy() error {
|
||||||
|
var err1 error
|
||||||
|
if err := remove(c.cpuacct.path); err != nil {
|
||||||
|
err1 = err
|
||||||
|
}
|
||||||
|
if err := remove(c.memory.path); err != nil {
|
||||||
|
err1 = err
|
||||||
|
}
|
||||||
|
if err := remove(c.pids.path); err != nil {
|
||||||
|
err1 = err
|
||||||
|
}
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CpuacctUsage read cpuacct.usage in ns
|
||||||
|
func (c *Cgroup) CpuacctUsage() (uint64, error) {
|
||||||
|
return c.cpuacct.ReadUint("cpuacct.usage")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryMaxUsageInBytes read memory.max_usage_in_bytes
|
||||||
|
func (c *Cgroup) MemoryMaxUsageInBytes() (uint64, error) {
|
||||||
|
return c.memory.ReadUint("memory.max_usage_in_bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMemoryLimitInBytes write memory.limit_in_bytes
|
||||||
|
func (c *Cgroup) SetMemoryLimitInBytes(i uint64) error {
|
||||||
|
return c.memory.WriteUint("memory.limit_in_bytes", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPidsMax write pids.max
|
||||||
|
func (c *Cgroup) SetPidsMax(i uint64) error {
|
||||||
|
return c.pids.WriteUint("pids.max", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCpuacctUsage write cpuacct.usage in ns
|
||||||
|
func (c *Cgroup) SetCpuacctUsage(i uint64) error {
|
||||||
|
return c.cpuacct.WriteUint("cpuacct.usage", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMemoryMaxUsageInBytes write cpuacct.usage in ns
|
||||||
|
func (c *Cgroup) SetMemoryMaxUsageInBytes(i uint64) error {
|
||||||
|
return c.memory.WriteUint("memory.max_usage_in_bytes", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindMemoryStatProperty find certain property from memory.stat
|
||||||
|
func (c *Cgroup) FindMemoryStatProperty(prop string) (uint64, error) {
|
||||||
|
content, err := c.memory.ReadFile("memory.stat")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
r := bytes.NewReader(content)
|
||||||
|
for {
|
||||||
|
var p string
|
||||||
|
var i uint64
|
||||||
|
_, err = fmt.Fscanln(r, &p, &i)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if p == prop {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(name string) error {
|
||||||
|
if name != "" {
|
||||||
|
return os.Remove(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -1,196 +0,0 @@
|
|||||||
package cgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const numberOfControllers = 5
|
|
||||||
|
|
||||||
// Controllers defines enabled controller of a cgroup
|
|
||||||
type Controllers struct {
|
|
||||||
CPU bool
|
|
||||||
CPUSet bool
|
|
||||||
CPUAcct bool
|
|
||||||
Memory bool
|
|
||||||
Pids bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set changes the enabled status of a specific controller
|
|
||||||
func (c *Controllers) Set(ct string, value bool) {
|
|
||||||
switch ct {
|
|
||||||
case CPU:
|
|
||||||
c.CPU = value
|
|
||||||
case CPUSet:
|
|
||||||
c.CPUSet = value
|
|
||||||
case CPUAcct:
|
|
||||||
c.CPUAcct = value
|
|
||||||
case Memory:
|
|
||||||
c.Memory = value
|
|
||||||
case Pids:
|
|
||||||
c.Pids = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intersect reset the specific controller if it is not enabled in the other
|
|
||||||
func (c *Controllers) Intersect(o *Controllers) {
|
|
||||||
c.CPU = c.CPU && o.CPU
|
|
||||||
c.CPUSet = c.CPUSet && o.CPUSet
|
|
||||||
c.CPUAcct = c.CPUAcct && o.CPUAcct
|
|
||||||
c.Memory = c.Memory && o.Memory
|
|
||||||
c.Pids = c.Pids && o.Pids
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains returns true if the current controller enabled all controllers in the other controller
|
|
||||||
func (c *Controllers) Contains(o *Controllers) bool {
|
|
||||||
return (c.CPU || !o.CPU) && (c.CPUSet || !o.CPUSet) && (c.CPUAcct || !o.CPUAcct) &&
|
|
||||||
(c.Memory || !o.Memory) && (c.Pids || !o.Pids)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Names returns a list of string of all enabled container names
|
|
||||||
func (c *Controllers) Names() []string {
|
|
||||||
names := make([]string, 0, numberOfControllers)
|
|
||||||
for _, v := range []struct {
|
|
||||||
e bool
|
|
||||||
n string
|
|
||||||
}{
|
|
||||||
{c.CPU, CPU},
|
|
||||||
{c.CPUAcct, CPUAcct},
|
|
||||||
{c.CPUSet, CPUSet},
|
|
||||||
{c.Memory, Memory},
|
|
||||||
{c.Pids, Pids},
|
|
||||||
} {
|
|
||||||
if v.e {
|
|
||||||
names = append(names, v.n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controllers) String() string {
|
|
||||||
return "[" + strings.Join(c.Names(), ", ") + "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info reads the cgroup mount info from /proc/cgroups
|
|
||||||
type Info struct {
|
|
||||||
Hierarchy int
|
|
||||||
NumCgroups int
|
|
||||||
Enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCgroupV1Info read /proc/cgroups and return the result
|
|
||||||
func GetCgroupV1Info() (map[string]Info, error) {
|
|
||||||
f, err := os.Open(procCgroupsPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
rt := make(map[string]Info)
|
|
||||||
s := bufio.NewScanner(f)
|
|
||||||
for s.Scan() {
|
|
||||||
text := s.Text()
|
|
||||||
if text[0] == '#' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.Fields(text)
|
|
||||||
if len(parts) < 4 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// format: subsys_name hierarchy num_cgroups enabled
|
|
||||||
name := parts[0]
|
|
||||||
hierarchy, err := strconv.Atoi(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
numCgroups, err := strconv.Atoi(parts[2])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
enabled := parts[3] != "0"
|
|
||||||
rt[name] = Info{
|
|
||||||
Hierarchy: hierarchy,
|
|
||||||
NumCgroups: numCgroups,
|
|
||||||
Enabled: enabled,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return rt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentCgroupPrefix returns the cgroup prefix of current process
|
|
||||||
func GetCurrentCgroupPrefix() (string, error) {
|
|
||||||
c, err := os.ReadFile(procSelfCgroup)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
firstLine, _, _ := strings.Cut(string(c), "\n")
|
|
||||||
f := strings.Split(firstLine, ":")
|
|
||||||
if len(f) < 3 {
|
|
||||||
return "", fmt.Errorf("invalid " + procSelfCgroup)
|
|
||||||
}
|
|
||||||
return f[2][1:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAvailableController returns available cgroup controller in the system
|
|
||||||
func GetAvailableController() (*Controllers, error) {
|
|
||||||
if DetectedCgroupType == TypeV1 {
|
|
||||||
return GetAvailableControllerV1()
|
|
||||||
}
|
|
||||||
return GetAvailableControllerV2()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAvailableControllerWithPrefix returns available cgroup controller within the cgroup prefix
|
|
||||||
func GetAvailableControllerWithPrefix(prefix string) (*Controllers, error) {
|
|
||||||
if DetectedCgroupType == TypeV1 {
|
|
||||||
return GetAvailableControllerV1()
|
|
||||||
}
|
|
||||||
return getAvailableControllerV2(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAvailableControllerV1 reads /proc/cgroups and get all available controller as set
|
|
||||||
func GetAvailableControllerV1() (*Controllers, error) {
|
|
||||||
info, err := GetCgroupV1Info()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rt := &Controllers{}
|
|
||||||
for k, v := range info {
|
|
||||||
if !v.Enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rt.Set(k, true)
|
|
||||||
}
|
|
||||||
return rt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAvailableControllerV2 reads /sys/fs/cgroup/cgroup.controllers to get all controller
|
|
||||||
func GetAvailableControllerV2() (*Controllers, error) {
|
|
||||||
return getAvailableControllerV2(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAvailableControllerV2(prefix string) (*Controllers, error) {
|
|
||||||
return getAvailableControllerV2path(filepath.Join(basePath, prefix, cgroupControllers))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAvailableControllerV2path(p string) (*Controllers, error) {
|
|
||||||
c, err := readFile(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &Controllers{}
|
|
||||||
f := strings.Fields(string(c))
|
|
||||||
for _, v := range f {
|
|
||||||
m.Set(v, true)
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
@ -1,234 +0,0 @@
|
|||||||
package cgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cgroup defines the common interface to control cgroups
|
|
||||||
// including v1 and v2 implementations.
|
|
||||||
// TODO: implement systemd integration
|
|
||||||
type Cgroup interface {
|
|
||||||
// AddProc add a process into the cgroup
|
|
||||||
AddProc(pid ...int) error
|
|
||||||
|
|
||||||
// Destroy deletes the cgroup
|
|
||||||
Destroy() error
|
|
||||||
|
|
||||||
// Existing returns true if the cgroup was opened rather than created
|
|
||||||
Existing() bool
|
|
||||||
|
|
||||||
//Nest creates a sub-cgroup, moves current process into that cgroup
|
|
||||||
Nest(name string) (Cgroup, error)
|
|
||||||
|
|
||||||
// CPUUsage reads total cpu usage of cgroup
|
|
||||||
CPUUsage() (uint64, error)
|
|
||||||
|
|
||||||
// MemoryUsage reads current total memory usage
|
|
||||||
MemoryUsage() (uint64, error)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// SetCpusetCpus sets the available cpu to use (cpuset.cpus).
|
|
||||||
SetCPUSet([]byte) error
|
|
||||||
|
|
||||||
// SetMemoryLimit sets memory.limit_in_bytes
|
|
||||||
SetMemoryLimit(uint64) error
|
|
||||||
|
|
||||||
// SetProcLimit sets pids.max
|
|
||||||
SetProcLimit(uint64) error
|
|
||||||
|
|
||||||
// Processes lists all existing process pid from the cgroup
|
|
||||||
Processes() ([]int, error)
|
|
||||||
|
|
||||||
// New creates a sub-cgroup based on the existing one
|
|
||||||
New(string) (Cgroup, error)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
var DetectedCgroupType = DetectType()
|
|
||||||
|
|
||||||
// New creates a new cgroup with provided prefix, it opens existing one if existed
|
|
||||||
func New(prefix string, ct *Controllers) (Cgroup, error) {
|
|
||||||
if DetectedCgroupType == TypeV1 {
|
|
||||||
return newV1(prefix, ct)
|
|
||||||
}
|
|
||||||
return newV2(prefix, ct)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loopV1Controllers(ct *Controllers, v1 *V1, f func(string, **v1controller) error) error {
|
|
||||||
for _, c := range []struct {
|
|
||||||
available bool
|
|
||||||
name string
|
|
||||||
cg **v1controller
|
|
||||||
}{
|
|
||||||
{ct.CPU, CPU, &v1.cpu},
|
|
||||||
{ct.CPUSet, CPUSet, &v1.cpuset},
|
|
||||||
{ct.CPUAcct, CPUAcct, &v1.cpuacct},
|
|
||||||
{ct.Memory, Memory, &v1.memory},
|
|
||||||
{ct.Pids, Pids, &v1.pids},
|
|
||||||
} {
|
|
||||||
if !c.available {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := f(c.name, c.cg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newV1(prefix string, ct *Controllers) (cg Cgroup, err error) {
|
|
||||||
v1 := &V1{
|
|
||||||
prefix: prefix,
|
|
||||||
}
|
|
||||||
// if failed, remove potential created directory
|
|
||||||
defer func() {
|
|
||||||
if err != nil && !v1.existing {
|
|
||||||
for _, p := range v1.all {
|
|
||||||
remove(p.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = loopV1Controllers(ct, v1, func(name string, cg **v1controller) error {
|
|
||||||
path, err := CreateV1ControllerPath(name, prefix)
|
|
||||||
*cg = newV1Controller(path)
|
|
||||||
if errors.Is(err, os.ErrExist) {
|
|
||||||
if len(v1.all) == 0 {
|
|
||||||
v1.existing = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v1.all = append(v1.all, *cg)
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// init cpu set before use, otherwise it is not functional
|
|
||||||
if v1.cpuset != nil {
|
|
||||||
if err = initCpuset(v1.cpuset.path); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newV2(prefix string, ct *Controllers) (cg Cgroup, err error) {
|
|
||||||
v2 := &V2{
|
|
||||||
path: filepath.Join(basePath, prefix),
|
|
||||||
control: ct,
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(v2.path); err == nil {
|
|
||||||
v2.existing = true
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err != nil && !v2.existing {
|
|
||||||
remove(v2.path)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// ensure controllers were enabled
|
|
||||||
s := ct.Names()
|
|
||||||
controlMsg := []byte("+" + strings.Join(s, " +"))
|
|
||||||
|
|
||||||
// start from base dir
|
|
||||||
entries := strings.Split(prefix, "/")
|
|
||||||
current := ""
|
|
||||||
for _, e := range entries {
|
|
||||||
parent := current
|
|
||||||
current = current + "/" + e
|
|
||||||
// try mkdir if not exists
|
|
||||||
if _, err := os.Stat(filepath.Join(basePath, current)); os.IsNotExist(err) {
|
|
||||||
if err := os.Mkdir(filepath.Join(basePath, current), dirPerm); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// no err means create success, need to enable it in its parent folder
|
|
||||||
ect, err := getAvailableControllerV2(current)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ect.Contains(ct) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := writeFile(filepath.Join(basePath, parent, cgroupSubtreeControl), controlMsg, filePerm); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v2, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenExisting opens a existing cgroup with provided prefix
|
|
||||||
func OpenExisting(prefix string, ct *Controllers) (Cgroup, error) {
|
|
||||||
if DetectedCgroupType == TypeV1 {
|
|
||||||
return openExistingV1(prefix, ct)
|
|
||||||
}
|
|
||||||
return openExistingV2(prefix, ct)
|
|
||||||
}
|
|
||||||
|
|
||||||
func openExistingV1(prefix string, ct *Controllers) (cg Cgroup, err error) {
|
|
||||||
v1 := &V1{
|
|
||||||
prefix: prefix,
|
|
||||||
existing: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = loopV1Controllers(ct, v1, func(name string, cg **v1controller) error {
|
|
||||||
p := filepath.Join(basePath, name, prefix)
|
|
||||||
*cg = newV1Controller(p)
|
|
||||||
// os.IsNotExist
|
|
||||||
if _, err := os.Stat(p); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v1.all = append(v1.all, *cg)
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// init cpu set before use, otherwise it is not functional
|
|
||||||
if v1.cpuset != nil {
|
|
||||||
if err = initCpuset(v1.cpuset.path); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func openExistingV2(prefix string, ct *Controllers) (cg Cgroup, err error) {
|
|
||||||
ect, err := getAvailableControllerV2(prefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !ect.Contains(ct) {
|
|
||||||
return nil, fmt.Errorf("open cgroup v2: requesting %v controllers but %v found", ct, ect)
|
|
||||||
}
|
|
||||||
return &V2{
|
|
||||||
path: filepath.Join(basePath, prefix),
|
|
||||||
control: ect,
|
|
||||||
existing: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
8
pkg/cgroup/consts.go
Normal file
8
pkg/cgroup/consts.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package cgroup
|
||||||
|
|
||||||
|
const (
|
||||||
|
// systemd mounted cgroups
|
||||||
|
basePath = "/sys/fs/cgroup"
|
||||||
|
cgroupProcs = "cgroup.procs"
|
||||||
|
procCgroupsPath = "/proc/cgroups"
|
||||||
|
)
|
||||||
@ -1,42 +0,0 @@
|
|||||||
package cgroup
|
|
||||||
|
|
||||||
// Cgroup constants
|
|
||||||
const (
|
|
||||||
// systemd mounted cgroups
|
|
||||||
basePath = "/sys/fs/cgroup"
|
|
||||||
cgroupProcs = "cgroup.procs"
|
|
||||||
procCgroupsPath = "/proc/cgroups"
|
|
||||||
procSelfCgroup = "/proc/self/cgroup"
|
|
||||||
|
|
||||||
cgroupSubtreeControl = "cgroup.subtree_control"
|
|
||||||
cgroupControllers = "cgroup.controllers"
|
|
||||||
|
|
||||||
filePerm = 0644
|
|
||||||
dirPerm = 0755
|
|
||||||
|
|
||||||
CPU = "cpu"
|
|
||||||
CPUAcct = "cpuacct"
|
|
||||||
CPUSet = "cpuset"
|
|
||||||
Memory = "memory"
|
|
||||||
Pids = "pids"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Type defines the version of cgroup
|
|
||||||
type Type int
|
|
||||||
|
|
||||||
// Type enum for cgroup
|
|
||||||
const (
|
|
||||||
TypeV1 = iota + 1
|
|
||||||
TypeV2
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t Type) String() string {
|
|
||||||
switch t {
|
|
||||||
case TypeV1:
|
|
||||||
return "v1"
|
|
||||||
case TypeV2:
|
|
||||||
return "v2"
|
|
||||||
default:
|
|
||||||
return "invalid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +1,17 @@
|
|||||||
// Package cgroup provides builder to create cgroup
|
// Package cgroup provices builder to create multiple different cgroup-v1 sub groups
|
||||||
// under systemd defined mount path (i.e.,sys/fs/cgroup) including v1 and
|
// under systemd defined mount path (i.e.,sys/fs/cgroup).
|
||||||
// v2 implementation.
|
|
||||||
//
|
//
|
||||||
// Available cgroup controller:
|
// Current available:
|
||||||
//
|
|
||||||
// cpu
|
|
||||||
// cpuset
|
|
||||||
// cpuacct
|
// cpuacct
|
||||||
// memory
|
// memory
|
||||||
// pids
|
// pids
|
||||||
//
|
//
|
||||||
// Current not available: devices, freezer, net_cls, blkio, perf_event, net_prio, huge_tlb, rdma
|
// Current not available: cpu, cpuset, devices, freezer, net_cls, blkio, perf_event, net_prio, huge_tlb, rdma
|
||||||
|
//
|
||||||
|
// Additional ideas:
|
||||||
|
//
|
||||||
|
// cpu share(not used): cpu.share
|
||||||
|
// reclaim pages from old process: memory.force_empty
|
||||||
|
// (tasks kill are managed out of cgroup as freeze takes some time)
|
||||||
|
// freeze: freezer.state
|
||||||
package cgroup
|
package cgroup
|
||||||
|
|||||||
69
pkg/cgroup/subcgroup.go
Normal file
69
pkg/cgroup/subcgroup.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package cgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubCgroup is the accessor for single cgroup resource with given path
|
||||||
|
type SubCgroup struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNotInitialized returned when trying to read from not initialized cgroup
|
||||||
|
var ErrNotInitialized = errors.New("cgroup was not initialized")
|
||||||
|
|
||||||
|
// NewSubCgroup creates a cgroup accessor with given path (path needs to be created in advance)
|
||||||
|
func NewSubCgroup(p string) *SubCgroup {
|
||||||
|
return &SubCgroup{path: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteUint writes uint64 into given file
|
||||||
|
func (c *SubCgroup) WriteUint(filename string, i uint64) error {
|
||||||
|
if c.path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.WriteFile(filename, []byte(strconv.FormatUint(i, 10)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadUint read uint64 from given file
|
||||||
|
func (c *SubCgroup) ReadUint(filename string) (uint64, error) {
|
||||||
|
if c.path == "" {
|
||||||
|
return 0, ErrNotInitialized
|
||||||
|
}
|
||||||
|
b, err := c.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
s, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile writes cgroup file and handles potential EINTR error while writes to
|
||||||
|
// the slow device (cgroup)
|
||||||
|
func (c *SubCgroup) WriteFile(name string, content []byte) error {
|
||||||
|
p := path.Join(c.path, name)
|
||||||
|
err := ioutil.WriteFile(p, content, 0664)
|
||||||
|
for err != nil && errors.Is(err, syscall.EINTR) {
|
||||||
|
err = ioutil.WriteFile(p, content, 0664)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile reads cgroup file and handles potential EINTR error while read to
|
||||||
|
// the slow device (cgroup)
|
||||||
|
func (c *SubCgroup) ReadFile(name string) ([]byte, error) {
|
||||||
|
p := path.Join(c.path, name)
|
||||||
|
data, err := ioutil.ReadFile(p)
|
||||||
|
for err != nil && errors.Is(err, syscall.EINTR) {
|
||||||
|
data, err = ioutil.ReadFile(p)
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
49
pkg/cgroup/utils.go
Normal file
49
pkg/cgroup/utils.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package cgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsureDirExists creates directories if the path not exists
|
||||||
|
func EnsureDirExists(path string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return os.Mkdir(path, os.ModePerm)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSubCgroupPath creates path for sub-cgroup with given group and prefix
|
||||||
|
func CreateSubCgroupPath(group, prefix string) (string, error) {
|
||||||
|
base := path.Join(basePath, group, prefix)
|
||||||
|
EnsureDirExists(base)
|
||||||
|
return ioutil.TempDir(base, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSubCgroup reads /proc/cgroups and get all available sub-cgroup as set
|
||||||
|
func GetAllSubCgroup() (map[string]bool, error) {
|
||||||
|
f, err := os.Open(procCgroupsPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
rt := make(map[string]bool)
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
for s.Scan() {
|
||||||
|
text := s.Text()
|
||||||
|
if text[0] != '#' {
|
||||||
|
parts := strings.Fields(text)
|
||||||
|
if len(parts) >= 4 && parts[3] != "0" {
|
||||||
|
rt[parts[0]] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rt, nil
|
||||||
|
}
|
||||||
@ -1,186 +0,0 @@
|
|||||||
package cgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"math/rand/v2"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnsureDirExists creates directories if the path not exists
|
|
||||||
func EnsureDirExists(path string) error {
|
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
||||||
return os.MkdirAll(path, dirPerm)
|
|
||||||
}
|
|
||||||
return os.ErrExist
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateV1ControllerPath create path for controller with given group, prefix
|
|
||||||
func CreateV1ControllerPath(controller, prefix string) (string, error) {
|
|
||||||
p := filepath.Join(basePath, controller, prefix)
|
|
||||||
return p, EnsureDirExists(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
const initPath = "init"
|
|
||||||
|
|
||||||
// EnableV2Nesting migrates all process in the container to nested /init path
|
|
||||||
// and enables all available controllers in the root cgroup
|
|
||||||
func EnableV2Nesting() error {
|
|
||||||
if DetectType() != TypeV2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := readFile(filepath.Join(basePath, cgroupProcs))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
procs := strings.Split(string(p), "\n")
|
|
||||||
if len(procs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mkdir init
|
|
||||||
if err := os.Mkdir(filepath.Join(basePath, initPath), dirPerm); err != nil && !errors.Is(err, os.ErrExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// move all process into init cgroup
|
|
||||||
procFile, err := os.OpenFile(filepath.Join(basePath, initPath, cgroupProcs), os.O_RDWR, filePerm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, v := range procs {
|
|
||||||
if _, err := procFile.WriteString(v); err != nil {
|
|
||||||
continue
|
|
||||||
//return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
procFile.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadProcesses reads cgroup.procs file and return pids individually
|
|
||||||
func ReadProcesses(path string) ([]int, error) {
|
|
||||||
content, err := readFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
procs := strings.Split(string(content), "\n")
|
|
||||||
rt := make([]int, len(procs))
|
|
||||||
for i, x := range procs {
|
|
||||||
if len(x) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rt[i], err = strconv.Atoi(x)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddProcesses add processes into cgroup.procs file
|
|
||||||
func AddProcesses(path string, procs []int) error {
|
|
||||||
f, err := os.OpenFile(path, os.O_RDWR, filePerm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
for _, p := range procs {
|
|
||||||
if _, err := f.WriteString(strconv.Itoa(p)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectType detects current mounted cgroup type in systemd default path
|
|
||||||
func DetectType() Type {
|
|
||||||
// if /sys/fs/cgroup is mounted as CGROUPV2 or TMPFS (V1)
|
|
||||||
var st unix.Statfs_t
|
|
||||||
if err := unix.Statfs(basePath, &st); err != nil {
|
|
||||||
// ignore errors, defaulting to CgroupV1
|
|
||||||
return TypeV1
|
|
||||||
}
|
|
||||||
if st.Type == unix.CGROUP2_SUPER_MAGIC {
|
|
||||||
return TypeV2
|
|
||||||
}
|
|
||||||
return TypeV1
|
|
||||||
}
|
|
||||||
|
|
||||||
func remove(name string) error {
|
|
||||||
if name != "" {
|
|
||||||
// os.Remove tried to Unlink, then Rmdir. Since we only delete directories, use
|
|
||||||
// Rmdir directly
|
|
||||||
return syscall.Rmdir(name)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var errPatternHasSeparator = errors.New("pattern contains path separator")
|
|
||||||
|
|
||||||
// prefixAndSuffix splits pattern by the last wildcard "*", if applicable,
|
|
||||||
// returning prefix as the part before "*" and suffix as the part after "*".
|
|
||||||
func prefixAndSuffix(pattern string) (prefix, suffix string, err error) {
|
|
||||||
for i := 0; i < len(pattern); i++ {
|
|
||||||
if os.IsPathSeparator(pattern[i]) {
|
|
||||||
return "", "", errPatternHasSeparator
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pos := strings.LastIndexByte(pattern, '*'); pos != -1 {
|
|
||||||
prefix, suffix = pattern[:pos], pattern[pos+1:]
|
|
||||||
} else {
|
|
||||||
prefix = pattern
|
|
||||||
}
|
|
||||||
return prefix, suffix, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readFile(p string) ([]byte, error) {
|
|
||||||
data, err := os.ReadFile(p)
|
|
||||||
for err != nil && errors.Is(err, syscall.EINTR) {
|
|
||||||
data, err = os.ReadFile(p)
|
|
||||||
}
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeFile(p string, content []byte, perm fs.FileMode) error {
|
|
||||||
err := os.WriteFile(p, content, perm)
|
|
||||||
for err != nil && errors.Is(err, syscall.EINTR) {
|
|
||||||
err = os.WriteFile(p, content, perm)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func nextRandom() string {
|
|
||||||
return strconv.Itoa(int(rand.Int32()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// randomBuild creates a cgroup with random directory, similar to os.MkdirTemp
|
|
||||||
func randomBuild(pattern string, build func(string) (Cgroup, error)) (Cgroup, error) {
|
|
||||||
prefix, suffix, err := prefixAndSuffix(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cgroup.builder: random %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
try := 0
|
|
||||||
for {
|
|
||||||
name := prefix + nextRandom() + suffix
|
|
||||||
cg, err := build(name)
|
|
||||||
if err == nil {
|
|
||||||
return cg, nil
|
|
||||||
}
|
|
||||||
if errors.Is(err, os.ErrExist) || (cg != nil && cg.Existing()) {
|
|
||||||
if try++; try < 10000 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("cgroup.builder: tried 10000 times but failed")
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("cgroup.builder: random %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,285 +0,0 @@
|
|||||||
package cgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Cgroup = &V1{}
|
|
||||||
|
|
||||||
// V1 is the combination of v1 controllers
|
|
||||||
type V1 struct {
|
|
||||||
prefix string
|
|
||||||
|
|
||||||
cpu *v1controller
|
|
||||||
cpuset *v1controller
|
|
||||||
cpuacct *v1controller
|
|
||||||
memory *v1controller
|
|
||||||
pids *v1controller
|
|
||||||
|
|
||||||
all []*v1controller
|
|
||||||
|
|
||||||
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 {
|
|
||||||
now *v1controller
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
{c.cpu, CPU},
|
|
||||||
{c.cpuset, CPUSet},
|
|
||||||
{c.cpuacct, CPUAcct},
|
|
||||||
{c.memory, Memory},
|
|
||||||
{c.pids, Pids},
|
|
||||||
} {
|
|
||||||
if v.now == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
names = append(names, v.name)
|
|
||||||
}
|
|
||||||
return "v1(" + c.prefix + ")[" + strings.Join(names, ", ") + "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddProc writes cgroup.procs to all controller
|
|
||||||
func (c *V1) AddProc(pids ...int) error {
|
|
||||||
for _, s := range c.all {
|
|
||||||
if err := s.AddProc(pids...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processes lists all existing process pid from the cgroup
|
|
||||||
func (c *V1) Processes() ([]int, error) {
|
|
||||||
if len(c.all) == 0 {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
return ReadProcesses(filepath.Join(c.all[0].path, cgroupProcs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a sub-cgroup based on the existing one
|
|
||||||
func (c *V1) New(name string) (cg Cgroup, err error) {
|
|
||||||
v1 := &V1{
|
|
||||||
prefix: filepath.Join(c.prefix, name),
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
for _, v := range v1.all {
|
|
||||||
remove(v.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for _, v := range []struct {
|
|
||||||
now *v1controller
|
|
||||||
new **v1controller
|
|
||||||
}{
|
|
||||||
{c.cpu, &v1.cpu},
|
|
||||||
{c.cpuset, &v1.cpuset},
|
|
||||||
{c.cpuacct, &v1.cpuacct},
|
|
||||||
{c.memory, &v1.memory},
|
|
||||||
{c.pids, &v1.pids},
|
|
||||||
} {
|
|
||||||
if v.now == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p := filepath.Join(v.now.path, name)
|
|
||||||
*v.new = &v1controller{path: p}
|
|
||||||
err = EnsureDirExists(p)
|
|
||||||
if os.IsExist(err) {
|
|
||||||
err = nil
|
|
||||||
if len(v1.all) == 0 {
|
|
||||||
v1.existing = true
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v1.all = append(v1.all, *v.new)
|
|
||||||
}
|
|
||||||
// init cpu set before use, otherwise it is not functional
|
|
||||||
if v1.cpuset != nil {
|
|
||||||
if err = initCpuset(v1.cpuset.path); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random creates a sub-cgroup based on the existing one but the name is randomly generated
|
|
||||||
func (c *V1) Random(pattern string) (Cgroup, error) {
|
|
||||||
return randomBuild(pattern, c.New)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nest creates a sub-cgroup, moves current process into that cgroup
|
|
||||||
func (c *V1) Nest(name string) (Cgroup, error) {
|
|
||||||
v1, err := c.New(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p, err := c.Processes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := v1.AddProc(p...); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return v1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy removes dir for controllers recursively, errors are ignored if remove one failed
|
|
||||||
func (c *V1) Destroy() error {
|
|
||||||
var err1 error
|
|
||||||
for _, s := range c.all {
|
|
||||||
if c.existing {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := remove(s.path); err != nil {
|
|
||||||
err1 = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Existing returns true if the cgroup was opened rather than created
|
|
||||||
func (c *V1) Existing() bool {
|
|
||||||
return c.existing
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCPUBandwidth set cpu quota via cfs interface
|
|
||||||
func (c *V1) SetCPUBandwidth(quota, period uint64) error {
|
|
||||||
if err := c.SetCPUCfsQuota(quota); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.SetCPUCfsPeriod(period)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCPUSet set cpuset.cpus
|
|
||||||
func (c *V1) SetCPUSet(b []byte) error {
|
|
||||||
return c.cpuset.WriteFile("cpuset.cpus", b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CPUUsage read cpuacct.usage in ns
|
|
||||||
func (c *V1) CPUUsage() (uint64, error) {
|
|
||||||
return c.cpuacct.ReadUint("cpuacct.usage")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryUsage read memory.usage_in_bytes
|
|
||||||
func (c *V1) MemoryUsage() (uint64, error) {
|
|
||||||
return c.memory.ReadUint("memory.usage_in_bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryMaxUsage read memory.max_usage_in_bytes
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetProcLimit write pids.max
|
|
||||||
func (c *V1) SetProcLimit(i uint64) error {
|
|
||||||
return c.pids.WriteUint("pids.max", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCpuacctUsage write cpuacct.usage in ns
|
|
||||||
func (c *V1) SetCpuacctUsage(i uint64) error {
|
|
||||||
return c.cpuacct.WriteUint("cpuacct.usage", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMemoryMaxUsageInBytes write cpuacct.usage in ns
|
|
||||||
func (c *V1) SetMemoryMaxUsageInBytes(i uint64) error {
|
|
||||||
return c.memory.WriteUint("memory.max_usage_in_bytes", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryMemswMaxUsageInBytes read memory.memsw.max_usage_in_bytes
|
|
||||||
func (c *V1) MemoryMemswMaxUsageInBytes() (uint64, error) {
|
|
||||||
return c.memory.ReadUint("memory.memsw.max_usage_in_bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMemoryMemswLimitInBytes write memory.memsw.limit_in_bytes
|
|
||||||
func (c *V1) SetMemoryMemswLimitInBytes(i uint64) error {
|
|
||||||
return c.memory.WriteUint("memory.memsw.limit_in_bytes", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCPUCfsPeriod set cpu.cfs_period_us in us
|
|
||||||
func (c *V1) SetCPUCfsPeriod(p uint64) error {
|
|
||||||
return c.cpu.WriteUint("cpu.cfs_period_us", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCPUCfsQuota set cpu.cfs_quota_us in us
|
|
||||||
func (c *V1) SetCPUCfsQuota(p uint64) error {
|
|
||||||
return c.cpu.WriteUint("cpu.cfs_quota_us", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCpusetMems set cpuset.mems
|
|
||||||
func (c *V1) SetCpusetMems(b []byte) error {
|
|
||||||
return c.cpuset.WriteFile("cpuset.mems", b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindMemoryStatProperty find certain property from memory.stat
|
|
||||||
func (c *V1) FindMemoryStatProperty(prop string) (uint64, error) {
|
|
||||||
content, err := c.memory.ReadFile("memory.stat")
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
r := bytes.NewReader(content)
|
|
||||||
for {
|
|
||||||
var p string
|
|
||||||
var i uint64
|
|
||||||
_, err = fmt.Fscanln(r, &p, &i)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if p == prop {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initCpuset will copy the config from the parent cpu sets if not exists
|
|
||||||
func initCpuset(path string) error {
|
|
||||||
for _, f := range []string{"cpuset.cpus", "cpuset.mems"} {
|
|
||||||
if err := copyCgroupPropertyFromParent(path, f); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyCgroupPropertyFromParent(path, name string) error {
|
|
||||||
// ensure current one empty
|
|
||||||
b, err := os.ReadFile(filepath.Join(path, name))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(bytes.TrimSpace(b)) > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// otherwise copy from parent, first to ensure it is empty by recursion
|
|
||||||
if err := copyCgroupPropertyFromParent(filepath.Dir(path), name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b, err = os.ReadFile(filepath.Join(filepath.Dir(path), name))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(filepath.Join(path, name), b, filePerm)
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
package cgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// v1controller is the accessor for single cgroup resource with given path
|
|
||||||
type v1controller struct {
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrNotInitialized returned when trying to read from not initialized cgroup
|
|
||||||
var ErrNotInitialized = errors.New("cgroup was not initialized")
|
|
||||||
|
|
||||||
// newV1Controller creates a cgroup accessor with given path (path needs to be created in advance)
|
|
||||||
func newV1Controller(p string) *v1controller {
|
|
||||||
return &v1controller{path: p}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteUint writes uint64 into given file
|
|
||||||
func (c *v1controller) WriteUint(filename string, i uint64) error {
|
|
||||||
if c == nil || c.path == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.WriteFile(filename, []byte(strconv.FormatUint(i, 10)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadUint read uint64 from given file
|
|
||||||
func (c *v1controller) ReadUint(filename string) (uint64, error) {
|
|
||||||
if c == nil || c.path == "" {
|
|
||||||
return 0, ErrNotInitialized
|
|
||||||
}
|
|
||||||
b, err := c.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
s, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFile writes cgroup file and handles potential EINTR error while writes to
|
|
||||||
// the slow device (cgroup)
|
|
||||||
func (c *v1controller) WriteFile(name string, content []byte) error {
|
|
||||||
if c == nil || c.path == "" {
|
|
||||||
return ErrNotInitialized
|
|
||||||
}
|
|
||||||
p := filepath.Join(c.path, name)
|
|
||||||
return writeFile(p, content, filePerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFile reads cgroup file and handles potential EINTR error while read to
|
|
||||||
// the slow device (cgroup)
|
|
||||||
func (c *v1controller) ReadFile(name string) ([]byte, error) {
|
|
||||||
if c == nil || c.path == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
p := filepath.Join(c.path, name)
|
|
||||||
return readFile(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v1controller) AddProc(pids ...int) error {
|
|
||||||
return AddProcesses(filepath.Join(c.path, cgroupProcs), pids)
|
|
||||||
}
|
|
||||||
@ -1,233 +0,0 @@
|
|||||||
package cgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// V2 provides cgroup interface for v2
|
|
||||||
type V2 struct {
|
|
||||||
path string
|
|
||||||
control *Controllers
|
|
||||||
subtreeOnce sync.Once
|
|
||||||
subtreeErr error
|
|
||||||
existing bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Cgroup = &V2{}
|
|
||||||
|
|
||||||
func (c *V2) Open() (*os.File, error) {
|
|
||||||
return os.OpenFile(c.path, 0, dirPerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *V2) String() string {
|
|
||||||
ct, _ := getAvailableControllerV2path(filepath.Join(c.path, cgroupControllers))
|
|
||||||
return "v2(" + c.path + ")" + ct.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddProc adds processes into the cgroup
|
|
||||||
func (c *V2) AddProc(pids ...int) error {
|
|
||||||
return AddProcesses(filepath.Join(c.path, cgroupProcs), pids)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processes returns all processes within the cgroup
|
|
||||||
func (c *V2) Processes() ([]int, error) {
|
|
||||||
return ReadProcesses(filepath.Join(c.path, cgroupProcs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a sub-cgroup based on the existing one
|
|
||||||
func (c *V2) New(name string) (Cgroup, error) {
|
|
||||||
if err := c.enableSubtreeControl(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v2 := &V2{
|
|
||||||
path: filepath.Join(c.path, name),
|
|
||||||
control: c.control,
|
|
||||||
}
|
|
||||||
if err := os.Mkdir(v2.path, dirPerm); err != nil {
|
|
||||||
if !os.IsExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v2.existing = true
|
|
||||||
}
|
|
||||||
return v2, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nest creates a sub-cgroup, moves current process into that cgroup
|
|
||||||
func (c *V2) Nest(name string) (Cgroup, error) {
|
|
||||||
v2 := &V2{
|
|
||||||
path: filepath.Join(c.path, name),
|
|
||||||
control: c.control,
|
|
||||||
}
|
|
||||||
if err := os.Mkdir(v2.path, dirPerm); err != nil {
|
|
||||||
if !os.IsExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v2.existing = true
|
|
||||||
}
|
|
||||||
p, err := c.Processes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := v2.AddProc(p...); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := c.enableSubtreeControl(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return v2, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *V2) enableSubtreeControl() error {
|
|
||||||
c.subtreeOnce.Do(func() {
|
|
||||||
ct, err := getAvailableControllerV2path(filepath.Join(c.path, cgroupControllers))
|
|
||||||
if err != nil {
|
|
||||||
c.subtreeErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ect, err := getAvailableControllerV2path(filepath.Join(c.path, cgroupSubtreeControl))
|
|
||||||
if err != nil {
|
|
||||||
c.subtreeErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ect.Contains(ct) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s := ct.Names()
|
|
||||||
controlMsg := []byte("+" + strings.Join(s, " +"))
|
|
||||||
c.subtreeErr = writeFile(filepath.Join(c.path, cgroupSubtreeControl), controlMsg, filePerm)
|
|
||||||
})
|
|
||||||
return c.subtreeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random creates a sub-cgroup based on the existing one but the name is randomly generated
|
|
||||||
func (c *V2) Random(pattern string) (Cgroup, error) {
|
|
||||||
return randomBuild(pattern, c.New)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy destroys the cgroup
|
|
||||||
func (c *V2) Destroy() error {
|
|
||||||
if !c.existing {
|
|
||||||
return remove(c.path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Existing returns true if the cgroup was opened rather than created
|
|
||||||
func (c *V2) Existing() bool {
|
|
||||||
return c.existing
|
|
||||||
}
|
|
||||||
|
|
||||||
// CPUUsage reads cpu.stat usage_usec
|
|
||||||
func (c *V2) CPUUsage() (uint64, error) {
|
|
||||||
b, err := c.ReadFile("cpu.stat")
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
s := bufio.NewScanner(bytes.NewReader(b))
|
|
||||||
for s.Scan() {
|
|
||||||
parts := strings.Fields(s.Text())
|
|
||||||
if len(parts) == 2 && parts[0] == "usage_usec" {
|
|
||||||
v, err := strconv.Atoi(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uint64(v) * 1000, nil // to ns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, os.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryUsage reads memory.current
|
|
||||||
func (c *V2) MemoryUsage() (uint64, error) {
|
|
||||||
if !c.control.Memory {
|
|
||||||
return 0, ErrNotInitialized
|
|
||||||
}
|
|
||||||
return c.ReadUint("memory.current")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryMaxUsage reads memory.peak
|
|
||||||
func (c *V2) MemoryMaxUsage() (uint64, error) {
|
|
||||||
if !c.control.Memory {
|
|
||||||
return 0, ErrNotInitialized
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
return ErrNotInitialized
|
|
||||||
}
|
|
||||||
content := strconv.FormatUint(quota, 10) + " " + strconv.FormatUint(period, 10)
|
|
||||||
return c.WriteFile("cpu.max", []byte(content))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCPUSet sets cpuset.cpus
|
|
||||||
func (c *V2) SetCPUSet(content []byte) error {
|
|
||||||
if !c.control.CPUSet {
|
|
||||||
return ErrNotInitialized
|
|
||||||
}
|
|
||||||
return c.WriteFile("cpuset.cpus", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMemoryLimit memory.max
|
|
||||||
func (c *V2) SetMemoryLimit(l uint64) error {
|
|
||||||
if !c.control.Memory {
|
|
||||||
return ErrNotInitialized
|
|
||||||
}
|
|
||||||
return c.WriteUint("memory.max", l)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetProcLimit pids.max
|
|
||||||
func (c *V2) SetProcLimit(l uint64) error {
|
|
||||||
if !c.control.Pids {
|
|
||||||
return ErrNotInitialized
|
|
||||||
}
|
|
||||||
return c.WriteUint("pids.max", l)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteUint writes uint64 into given file
|
|
||||||
func (c *V2) WriteUint(filename string, i uint64) error {
|
|
||||||
return c.WriteFile(filename, []byte(strconv.FormatUint(i, 10)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadUint read uint64 from given file
|
|
||||||
func (c *V2) ReadUint(filename string) (uint64, error) {
|
|
||||||
b, err := c.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
s, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFile writes cgroup file and handles potential EINTR error while writes to
|
|
||||||
// the slow device (cgroup)
|
|
||||||
func (c *V2) WriteFile(name string, content []byte) error {
|
|
||||||
p := filepath.Join(c.path, name)
|
|
||||||
return writeFile(p, content, filePerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFile reads cgroup file and handles potential EINTR error while read to
|
|
||||||
// the slow device (cgroup)
|
|
||||||
func (c *V2) ReadFile(name string) ([]byte, error) {
|
|
||||||
p := filepath.Join(c.path, name)
|
|
||||||
return readFile(p)
|
|
||||||
}
|
|
||||||
@ -1,12 +1,13 @@
|
|||||||
package forkexec
|
package darwin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWrite(t *testing.T) {
|
func TestWrite(t *testing.T) {
|
||||||
c, err := os.ReadFile("test.sb")
|
c, err := ioutil.ReadFile("test.sb")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package forkexec
|
package darwin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
39
pkg/darwin/syscall_darwin.go
Normal file
39
pkg/darwin/syscall_darwin.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package darwin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SandboxInit calls sandbox_init
|
||||||
|
func SandboxInit(profile *byte, flags uint64, errorBuf **byte) (err error) {
|
||||||
|
var r1 uintptr
|
||||||
|
r1, _, err = syscall3(funcPC(libc_sandbox_init_trampoline), uintptr(unsafe.Pointer(profile)), uintptr(flags), uintptr(unsafe.Pointer(errorBuf)))
|
||||||
|
if r1 != 0 {
|
||||||
|
err = syscall.EINVAL
|
||||||
|
} else {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SandboxFreeError calls sandbox_free_error
|
||||||
|
func SandboxFreeError(errorBuf *byte) {
|
||||||
|
syscall3(funcPC(libc_sandbox_free_error_trampoline), uintptr(unsafe.Pointer(errorBuf)), 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func libc_sandbox_init_trampoline()
|
||||||
|
|
||||||
|
//go:linkname libc_sandbox_init libc_sandbox_init
|
||||||
|
//go:cgo_import_dynamic libc_sandbox_init sandbox_init "/usr/lib/libSystem.B.dylib"
|
||||||
|
|
||||||
|
func libc_sandbox_free_error_trampoline()
|
||||||
|
|
||||||
|
//go:linkname libc_sandbox_free_error libc_sandbox_free_error
|
||||||
|
//go:cgo_import_dynamic libc_sandbox_free_error sandbox_free_error "/usr/lib/libSystem.B.dylib"
|
||||||
|
|
||||||
|
//go:linkname syscall3 syscall.syscall
|
||||||
|
func syscall3(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
||||||
|
|
||||||
|
//go:linkname funcPC syscall.funcPC
|
||||||
|
func funcPC(f func()) uintptr
|
||||||
6
pkg/darwin/zsyscall_darwin.s
Normal file
6
pkg/darwin/zsyscall_darwin.s
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·libc_sandbox_init_trampoline(SB),NOSPLIT,$0-0
|
||||||
|
JMP libc_sandbox_init(SB)
|
||||||
|
|
||||||
|
TEXT ·libc_sandbox_free_error_trampoline(SB),NOSPLIT,$0-0
|
||||||
|
JMP libc_sandbox_free_error(SB)
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package forkexec
|
package forkexec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@ -19,43 +20,6 @@ var (
|
|||||||
defaultBind = []string{"/usr", "/lib", "/lib64", "/bin"}
|
defaultBind = []string{"/usr", "/lib", "/lib64", "/bin"}
|
||||||
)
|
)
|
||||||
|
|
||||||
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"},
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
wait4(pid, b)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkSimpleFork is about 0.70ms/op
|
// BenchmarkSimpleFork is about 0.70ms/op
|
||||||
func BenchmarkSimpleFork(b *testing.B) {
|
func BenchmarkSimpleFork(b *testing.B) {
|
||||||
r, f := getRunner(b)
|
r, f := getRunner(b)
|
||||||
@ -121,7 +85,7 @@ func BenchmarkUnshareNet(b *testing.B) {
|
|||||||
|
|
||||||
// BenchmarkFastUnshareMountPivot is about 104ms/op
|
// BenchmarkFastUnshareMountPivot is about 104ms/op
|
||||||
func BenchmarkFastUnshareMountPivot(b *testing.B) {
|
func BenchmarkFastUnshareMountPivot(b *testing.B) {
|
||||||
root, err := os.MkdirTemp("", "ns")
|
root, err := ioutil.TempDir("", "ns")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Errorf("failed to create temp dir")
|
b.Errorf("failed to create temp dir")
|
||||||
}
|
}
|
||||||
@ -148,7 +112,7 @@ func BenchmarkUnshareAll(b *testing.B) {
|
|||||||
|
|
||||||
// BenchmarkUnshareMountPivot is about 880ms/op
|
// BenchmarkUnshareMountPivot is about 880ms/op
|
||||||
func BenchmarkUnshareMountPivot(b *testing.B) {
|
func BenchmarkUnshareMountPivot(b *testing.B) {
|
||||||
root, err := os.MkdirTemp("", "ns")
|
root, err := ioutil.TempDir("", "ns")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Errorf("failed to create temp dir")
|
b.Errorf("failed to create temp dir")
|
||||||
}
|
}
|
||||||
@ -175,15 +139,13 @@ func getRunner(b *testing.B) (*Runner, *os.File) {
|
|||||||
|
|
||||||
func benchmarkRun(r *Runner, b *testing.B) {
|
func benchmarkRun(r *Runner, b *testing.B) {
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
for i := 0; i < b.N; i++ {
|
||||||
for pb.Next() {
|
|
||||||
pid, err := r.Start()
|
pid, err := r.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fail()
|
||||||
}
|
}
|
||||||
wait4(pid, b)
|
wait4(pid, b)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMounts(dirs []string) []mount.SyscallParams {
|
func getMounts(dirs []string) []mount.SyscallParams {
|
||||||
@ -195,7 +157,7 @@ func getMounts(dirs []string) []mount.SyscallParams {
|
|||||||
Flags: roBind,
|
Flags: roBind,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
m, _ := builder.FilterNotExist().Build()
|
m, _ := builder.Build(true)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -46,11 +46,6 @@ var (
|
|||||||
Permitted: 0,
|
Permitted: 0,
|
||||||
Inheritable: 0,
|
Inheritable: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1ms
|
|
||||||
etxtbsyRetryInterval = unix.Timespec{
|
|
||||||
Nsec: 1 * 1000 * 1000,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@ -1,102 +0,0 @@
|
|||||||
package forkexec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrorLocation defines the location where child process failed to exec
|
|
||||||
type ErrorLocation int
|
|
||||||
|
|
||||||
// ChildError defines the specific error and location where it failed
|
|
||||||
type ChildError struct {
|
|
||||||
Err syscall.Errno
|
|
||||||
Location ErrorLocation
|
|
||||||
Index int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Location constants
|
|
||||||
const (
|
|
||||||
LocClone ErrorLocation = iota + 1
|
|
||||||
LocCloseWrite
|
|
||||||
LocUnshareUserRead
|
|
||||||
LocGetPid
|
|
||||||
LocKeepCapability
|
|
||||||
LocSetGroups
|
|
||||||
LocSetGid
|
|
||||||
LocSetUid
|
|
||||||
LocDup3
|
|
||||||
LocFcntl
|
|
||||||
LocSetSid
|
|
||||||
LocIoctl
|
|
||||||
LocMountRoot
|
|
||||||
LocMountTmpfs
|
|
||||||
LocMountChdir
|
|
||||||
LocMount
|
|
||||||
LocMountMkdir
|
|
||||||
LocPivotRoot
|
|
||||||
LocUmount
|
|
||||||
LocUnlink
|
|
||||||
LocMountRootReadonly
|
|
||||||
LocChdir
|
|
||||||
LocSetRlimit
|
|
||||||
LocSetNoNewPrivs
|
|
||||||
LocDropCapability
|
|
||||||
LocSetCap
|
|
||||||
LocPtraceMe
|
|
||||||
LocStop
|
|
||||||
LocSeccomp
|
|
||||||
LocSyncWrite
|
|
||||||
LocSyncRead
|
|
||||||
LocExecve
|
|
||||||
)
|
|
||||||
|
|
||||||
var locToString = []string{
|
|
||||||
"unknown",
|
|
||||||
"clone",
|
|
||||||
"close_write",
|
|
||||||
"unshare_user_read",
|
|
||||||
"getpid",
|
|
||||||
"keep_capability",
|
|
||||||
"setgroups",
|
|
||||||
"setgid",
|
|
||||||
"setuid",
|
|
||||||
"dup3",
|
|
||||||
"fcntl",
|
|
||||||
"setsid",
|
|
||||||
"ioctl",
|
|
||||||
"mount(root)",
|
|
||||||
"mount(tmpfs)",
|
|
||||||
"mount(chdir)",
|
|
||||||
"mount",
|
|
||||||
"mount(mkdir)",
|
|
||||||
"pivot_root",
|
|
||||||
"umount",
|
|
||||||
"unlink",
|
|
||||||
"mount(readonly)",
|
|
||||||
"chdir",
|
|
||||||
"setrlimt",
|
|
||||||
"set_no_new_privs",
|
|
||||||
"drop_capability",
|
|
||||||
"set_cap",
|
|
||||||
"ptrace_me",
|
|
||||||
"stop",
|
|
||||||
"seccomp",
|
|
||||||
"sync_write",
|
|
||||||
"sync_read",
|
|
||||||
"execve",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrorLocation) String() string {
|
|
||||||
if e >= LocClone && e <= LocExecve {
|
|
||||||
return locToString[e]
|
|
||||||
}
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ChildError) Error() string {
|
|
||||||
if e.Index > 0 {
|
|
||||||
return fmt.Sprintf("%s(%d): %s", e.Location.String(), e.Index, e.Err.Error())
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s: %s", e.Location.String(), e.Err.Error())
|
|
||||||
}
|
|
||||||
1
pkg/forkexec/fork.s
Normal file
1
pkg/forkexec/fork.s
Normal file
@ -0,0 +1 @@
|
|||||||
|
// to use go:linkname
|
||||||
@ -22,7 +22,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, prof
|
|||||||
beforeFork()
|
beforeFork()
|
||||||
|
|
||||||
// UnshareFlags (new namespaces) is activated by clone syscall
|
// UnshareFlags (new namespaces) is activated by clone syscall
|
||||||
r1, _, err1 = rawSyscall(libc_fork_trampoline_addr, 0, 0, 0)
|
r1, _, err1 = rawSyscall(funcPC(libc_fork_trampoline), 0, 0, 0)
|
||||||
if err1 != 0 || r1 != 0 {
|
if err1 != 0 || r1 != 0 {
|
||||||
// in parent process, immediate return
|
// in parent process, immediate return
|
||||||
return
|
return
|
||||||
@ -33,24 +33,18 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, prof
|
|||||||
// Notice: cannot call any GO functions beyond this point
|
// Notice: cannot call any GO functions beyond this point
|
||||||
|
|
||||||
// Close write end of pipe
|
// Close write end of pipe
|
||||||
if _, _, err1 = rawSyscall(libc_close_trampoline_addr, uintptr(p[0]), 0, 0); err1 != 0 {
|
if _, _, err1 = rawSyscall(funcPC(libc_close_trampoline), uintptr(p[0]), 0, 0); err1 != 0 {
|
||||||
goto childerror
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set pg id
|
|
||||||
_, _, err1 = rawSyscall(libc_setpgid_trampoline_addr, 0, 0, 0)
|
|
||||||
if err1 != 0 {
|
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 1 & pass 2 assigns fds for child process
|
// Pass 1 & pass 2 assigns fds for child process
|
||||||
// Pass 1: fd[i] < i => nextfd
|
// Pass 1: fd[i] < i => nextfd
|
||||||
if pipe < nextfd {
|
if pipe < nextfd {
|
||||||
_, _, err1 = rawSyscall(libc_dup2_trampoline_addr, uintptr(pipe), uintptr(nextfd), 0)
|
_, _, err1 = rawSyscall(funcPC(libc_dup2_trampoline), uintptr(pipe), uintptr(nextfd), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
rawSyscall(libc_fcntl_trampoline_addr, uintptr(nextfd), syscall.F_SETFD, syscall.FD_CLOEXEC)
|
rawSyscall(funcPC(libc_fcntl_trampoline), uintptr(nextfd), syscall.F_SETFD, syscall.FD_CLOEXEC)
|
||||||
pipe = nextfd
|
pipe = nextfd
|
||||||
nextfd++
|
nextfd++
|
||||||
}
|
}
|
||||||
@ -60,11 +54,11 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, prof
|
|||||||
if nextfd == pipe {
|
if nextfd == pipe {
|
||||||
nextfd++
|
nextfd++
|
||||||
}
|
}
|
||||||
_, _, err1 = rawSyscall(libc_dup2_trampoline_addr, uintptr(fd[i]), uintptr(nextfd), 0)
|
_, _, err1 = rawSyscall(funcPC(libc_dup2_trampoline), uintptr(fd[i]), uintptr(nextfd), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
rawSyscall(libc_fcntl_trampoline_addr, uintptr(nextfd), syscall.F_SETFD, syscall.FD_CLOEXEC)
|
rawSyscall(funcPC(libc_fcntl_trampoline), uintptr(nextfd), syscall.F_SETFD, syscall.FD_CLOEXEC)
|
||||||
// Set up close on exec
|
// Set up close on exec
|
||||||
fd[i] = nextfd
|
fd[i] = nextfd
|
||||||
nextfd++
|
nextfd++
|
||||||
@ -73,18 +67,18 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, prof
|
|||||||
// Pass 2: fd[i] => i
|
// Pass 2: fd[i] => i
|
||||||
for i := 0; i < len(fd); i++ {
|
for i := 0; i < len(fd); i++ {
|
||||||
if fd[i] == -1 {
|
if fd[i] == -1 {
|
||||||
rawSyscall(libc_close_trampoline_addr, uintptr(i), 0, 0)
|
rawSyscall(funcPC(libc_close_trampoline), uintptr(i), 0, 0)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if fd[i] == int(i) {
|
if fd[i] == int(i) {
|
||||||
// dup2(i, i) will not clear close on exec flag, need to reset the flag
|
// dup2(i, i) will not clear close on exec flag, need to reset the flag
|
||||||
_, _, err1 = rawSyscall(libc_fcntl_trampoline_addr, uintptr(fd[i]), syscall.F_SETFD, 0)
|
_, _, err1 = rawSyscall(funcPC(libc_fcntl_trampoline), uintptr(fd[i]), syscall.F_SETFD, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, _, err1 = rawSyscall(libc_dup2_trampoline_addr, uintptr(fd[i]), uintptr(i), 0)
|
_, _, err1 = rawSyscall(funcPC(libc_dup2_trampoline), uintptr(fd[i]), uintptr(i), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
@ -92,7 +86,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, prof
|
|||||||
|
|
||||||
// chdir for child
|
// chdir for child
|
||||||
if workdir != nil {
|
if workdir != nil {
|
||||||
_, _, err1 = rawSyscall(libc_chdir_trampoline_addr, uintptr(unsafe.Pointer(workdir)), 0, 0)
|
_, _, err1 = rawSyscall(funcPC(libc_chdir_trampoline), uintptr(unsafe.Pointer(workdir)), 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
@ -100,18 +94,15 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, prof
|
|||||||
|
|
||||||
// Set limit
|
// Set limit
|
||||||
for _, rlim := range r.RLimits {
|
for _, rlim := range r.RLimits {
|
||||||
_, _, err1 = rawSyscall(libc_setrlimit_trampoline_addr, uintptr(rlim.Res), uintptr(unsafe.Pointer(&rlim.Rlim)), 0)
|
_, _, err1 = rawSyscall(funcPC(libc_setrlimit_trampoline), uintptr(rlim.Res), uintptr(unsafe.Pointer(&rlim.Rlim)), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
if err1 == syscall.EINVAL && (rlim.Res == syscall.RLIMIT_DATA || rlim.Res == syscall.RLIMIT_AS) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load sandbox profile
|
// Load sandbox profile
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
r1, _, err1 = rawSyscall(libc_sandbox_init_trampoline_addr, uintptr(unsafe.Pointer(profile)), 0, uintptr(unsafe.Pointer(&errBuf)))
|
r1, _, err1 = rawSyscall(funcPC(libc_sandbox_init_trampoline), uintptr(unsafe.Pointer(profile)), 0, uintptr(unsafe.Pointer(&errBuf)))
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
@ -119,31 +110,31 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, prof
|
|||||||
err1 = 253
|
err1 = 253
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
rawSyscall(libc_sandbox_free_error_trampoline_addr, uintptr(unsafe.Pointer(errBuf)), 0, 0)
|
rawSyscall(funcPC(libc_sandbox_free_error_trampoline), uintptr(unsafe.Pointer(errBuf)), 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync before exec
|
// Sync before exec
|
||||||
err2 = 0
|
err2 = 0
|
||||||
r1, _, err1 = rawSyscall(libc_write_trampoline_addr, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), unsafe.Sizeof(err2))
|
r1, _, err1 = rawSyscall(funcPC(libc_write_trampoline), uintptr(pipe), uintptr(unsafe.Pointer(&err2)), unsafe.Sizeof(err2))
|
||||||
if r1 == 0 || err1 != 0 {
|
if r1 == 0 || err1 != 0 {
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
r1, _, err1 = rawSyscall(libc_read_trampoline_addr, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), unsafe.Sizeof(err2))
|
r1, _, err1 = rawSyscall(funcPC(libc_read_trampoline), uintptr(pipe), uintptr(unsafe.Pointer(&err2)), unsafe.Sizeof(err2))
|
||||||
if r1 == 0 || err1 != 0 {
|
if r1 == 0 || err1 != 0 {
|
||||||
goto childerror
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time to exec.
|
// Time to exec.
|
||||||
_, _, err1 = rawSyscall(libc_execve_trampoline_addr,
|
_, _, err1 = rawSyscall(funcPC(libc_execve_trampoline),
|
||||||
uintptr(unsafe.Pointer(argv0)),
|
uintptr(unsafe.Pointer(argv0)),
|
||||||
uintptr(unsafe.Pointer(&argv[0])),
|
uintptr(unsafe.Pointer(&argv[0])),
|
||||||
uintptr(unsafe.Pointer(&env[0])))
|
uintptr(unsafe.Pointer(&env[0])))
|
||||||
|
|
||||||
childerror:
|
childerror:
|
||||||
// send error code on pipe
|
// send error code on pipe
|
||||||
rawSyscall(libc_write_trampoline_addr, uintptr(pipe), uintptr(unsafe.Pointer(&err1)), unsafe.Sizeof(err1))
|
rawSyscall(funcPC(libc_write_trampoline), uintptr(pipe), uintptr(unsafe.Pointer(&err1)), unsafe.Sizeof(err1))
|
||||||
for {
|
for {
|
||||||
rawSyscall(libc_exit_trampoline_addr, uintptr(err1+err2), 0, 0)
|
rawSyscall(funcPC(libc_exit_trampoline), uintptr(err1+err2), 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,24 @@
|
|||||||
package forkexec
|
package forkexec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/forkexec/vfork"
|
|
||||||
"github.com/criyle/go-sandbox/pkg/rlimit"
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reference to src/syscall/exec_linux.go
|
// Reference to src/syscall/exec_linux.go
|
||||||
//
|
|
||||||
//go:noinline
|
|
||||||
//go:norace
|
//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) {
|
func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, hostname, domainname, pivotRoot *byte, p [2]int) (r1 uintptr, err1 syscall.Errno) {
|
||||||
var (
|
var (
|
||||||
clone3 *cloneArgs
|
|
||||||
pid uintptr
|
pid uintptr
|
||||||
err2 syscall.Errno
|
err2 syscall.Errno
|
||||||
unshareUser = r.CloneFlags&unix.CLONE_NEWUSER == unix.CLONE_NEWUSER
|
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
|
// similar to exec_linux, avoid side effect by shuffling around
|
||||||
fd, nextfd := prepareFds(r.Files)
|
fd, nextfd := prepareFds(r.Files)
|
||||||
|
pipe := p[1]
|
||||||
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
|
// Acquire the fork lock so that no other threads
|
||||||
// create new fds that are not yet close-on-exec
|
// create new fds that are not yet close-on-exec
|
||||||
@ -54,16 +30,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
beforeFork()
|
beforeFork()
|
||||||
|
|
||||||
// UnshareFlags (new namespaces) is activated by clone syscall
|
// UnshareFlags (new namespaces) is activated by clone syscall
|
||||||
if clone3 != nil {
|
r1, _, err1 = syscall.RawSyscall6(syscall.SYS_CLONE, uintptr(syscall.SIGCHLD)|(r.CloneFlags&UnshareFlags), 0, 0, 0, 0, 0)
|
||||||
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 {
|
if err1 != 0 || r1 != 0 {
|
||||||
// in parent process, immediate return
|
// in parent process, immediate return
|
||||||
return
|
return
|
||||||
@ -75,7 +42,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
|
|
||||||
// Close write end of pipe
|
// Close write end of pipe
|
||||||
if _, _, err1 = syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(p[0]), 0, 0); err1 != 0 {
|
if _, _, err1 = syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(p[0]), 0, 0); err1 != 0 {
|
||||||
childExitError(pipe, LocCloseWrite, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// If usernamespace is unshared, uid map and gid map is required to create folders
|
// If usernamespace is unshared, uid map and gid map is required to create folders
|
||||||
@ -86,31 +53,29 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
if unshareUser {
|
if unshareUser {
|
||||||
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), unsafe.Sizeof(err2))
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), unsafe.Sizeof(err2))
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocUnshareUserRead, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
if r1 != unsafe.Sizeof(err2) {
|
if r1 != unsafe.Sizeof(err2) {
|
||||||
err1 = syscall.EINVAL
|
err1 = syscall.EINVAL
|
||||||
childExitError(pipe, LocUnshareUserRead, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
if err2 != 0 {
|
if err2 != 0 {
|
||||||
err1 = err2
|
err1 = err2
|
||||||
childExitError(pipe, LocUnshareUserRead, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get pid of child
|
// Get pid of child
|
||||||
pid, _, err1 = syscall.RawSyscall(syscall.SYS_GETPID, 0, 0, 0)
|
pid, _, err1 = syscall.RawSyscall(syscall.SYS_GETPID, 0, 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocGetPid, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep capabilities through set_uid / set_gid calls (make sure we can use unshare cgroup), later dropped
|
// keep capabilities through set_uid / set_gid calls (make sure we can use unshare cgroup), later dropped
|
||||||
if r.Credential != nil || r.UnshareCgroupAfterSync {
|
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECUREBITS,
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECUREBITS,
|
||||||
_SECURE_KEEP_CAPS_LOCKED|_SECURE_NO_SETUID_FIXUP|_SECURE_NO_SETUID_FIXUP_LOCKED, 0)
|
_SECURE_KEEP_CAPS_LOCKED|_SECURE_NO_SETUID_FIXUP|_SECURE_NO_SETUID_FIXUP_LOCKED, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocKeepCapability, err1)
|
goto childerror
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the credential for the child process(exec_linux.go)
|
// set the credential for the child process(exec_linux.go)
|
||||||
@ -123,16 +88,16 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
if !(r.GIDMappings != nil && !r.GIDMappingsEnableSetgroups && ngroups == 0) && !cred.NoSetGroups {
|
if !(r.GIDMappings != nil && !r.GIDMappingsEnableSetgroups && ngroups == 0) && !cred.NoSetGroups {
|
||||||
_, _, err1 = syscall.RawSyscall(unix.SYS_SETGROUPS, ngroups, groups, 0)
|
_, _, err1 = syscall.RawSyscall(unix.SYS_SETGROUPS, ngroups, groups, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSetGroups, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, _, err1 = syscall.RawSyscall(unix.SYS_SETGID, uintptr(cred.Gid), 0, 0)
|
_, _, err1 = syscall.RawSyscall(unix.SYS_SETGID, uintptr(cred.Gid), 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSetGid, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
_, _, err1 = syscall.RawSyscall(unix.SYS_SETUID, uintptr(cred.Uid), 0, 0)
|
_, _, err1 = syscall.RawSyscall(unix.SYS_SETUID, uintptr(cred.Uid), 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSetUid, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,24 +106,20 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
if pipe < nextfd {
|
if pipe < nextfd {
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(pipe), uintptr(nextfd), syscall.O_CLOEXEC)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(pipe), uintptr(nextfd), syscall.O_CLOEXEC)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocDup3, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
pipe = nextfd
|
pipe = nextfd
|
||||||
nextfd++
|
nextfd++
|
||||||
}
|
}
|
||||||
if r.ExecFile > 0 && int(r.ExecFile) < nextfd {
|
if r.ExecFile > 0 && int(r.ExecFile) < nextfd {
|
||||||
// Avoid fd rewrite
|
|
||||||
for nextfd == pipe {
|
|
||||||
nextfd++
|
|
||||||
}
|
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, r.ExecFile, uintptr(nextfd), syscall.O_CLOEXEC)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, r.ExecFile, uintptr(nextfd), syscall.O_CLOEXEC)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocDup3, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
r.ExecFile = uintptr(nextfd)
|
r.ExecFile = uintptr(nextfd)
|
||||||
nextfd++
|
nextfd++
|
||||||
}
|
}
|
||||||
for i = 0; i < len(fd); i++ {
|
for i := 0; i < len(fd); i++ {
|
||||||
if fd[i] >= 0 && fd[i] < int(i) {
|
if fd[i] >= 0 && fd[i] < int(i) {
|
||||||
// Avoid fd rewrite
|
// Avoid fd rewrite
|
||||||
for nextfd == pipe || (r.ExecFile > 0 && nextfd == int(r.ExecFile)) {
|
for nextfd == pipe || (r.ExecFile > 0 && nextfd == int(r.ExecFile)) {
|
||||||
@ -166,7 +127,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
}
|
}
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(fd[i]), uintptr(nextfd), syscall.O_CLOEXEC)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(fd[i]), uintptr(nextfd), syscall.O_CLOEXEC)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocDup3, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
// Set up close on exec
|
// Set up close on exec
|
||||||
fd[i] = nextfd
|
fd[i] = nextfd
|
||||||
@ -174,7 +135,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Pass 2: fd[i] => i
|
// Pass 2: fd[i] => i
|
||||||
for i = 0; i < len(fd); i++ {
|
for i := 0; i < len(fd); i++ {
|
||||||
if fd[i] == -1 {
|
if fd[i] == -1 {
|
||||||
syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(i), 0, 0)
|
syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(i), 0, 0)
|
||||||
continue
|
continue
|
||||||
@ -183,39 +144,30 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
// dup2(i, i) will not clear close on exec flag, need to reset the flag
|
// dup2(i, i) will not clear close on exec flag, need to reset the flag
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_FCNTL, uintptr(fd[i]), syscall.F_SETFD, 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_FCNTL, uintptr(fd[i]), syscall.F_SETFD, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocFcntl, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(fd[i]), uintptr(i), 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(fd[i]), uintptr(i), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocDup3, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the session ID
|
// Set the pgid, so that the wait operation can apply to only certain
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_SETSID, 0, 0, 0)
|
// subgroup of processes
|
||||||
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_SETPGID, 0, 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSetSid, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the controlling TTY
|
|
||||||
if r.CTTY {
|
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_IOCTL, uintptr(0), uintptr(syscall.TIOCSCTTY), 1)
|
|
||||||
if err1 != 0 {
|
|
||||||
childExitError(pipe, LocIoctl, err1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mount file system
|
|
||||||
{
|
|
||||||
// If mount point is unshared, mark root as private to avoid propagate
|
// If mount point is unshared, mark root as private to avoid propagate
|
||||||
// outside to the original mount namespace
|
// outside to the original mount namespace
|
||||||
if r.CloneFlags&syscall.CLONE_NEWNS == syscall.CLONE_NEWNS {
|
if r.CloneFlags&syscall.CLONE_NEWNS == syscall.CLONE_NEWNS {
|
||||||
_, _, err1 = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(&none[0])),
|
_, _, err1 = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(&none[0])),
|
||||||
uintptr(unsafe.Pointer(&slash[0])), 0, syscall.MS_REC|syscall.MS_PRIVATE, 0, 0)
|
uintptr(unsafe.Pointer(&slash[0])), 0, syscall.MS_REC|syscall.MS_PRIVATE, 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocMountRoot, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,30 +178,30 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
uintptr(unsafe.Pointer(pivotRoot)), uintptr(unsafe.Pointer(&tmpfs[0])), 0,
|
uintptr(unsafe.Pointer(pivotRoot)), uintptr(unsafe.Pointer(&tmpfs[0])), 0,
|
||||||
uintptr(unsafe.Pointer(&empty[0])), 0)
|
uintptr(unsafe.Pointer(&empty[0])), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocMountTmpfs, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(pivotRoot)), 0, 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(pivotRoot)), 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocMountChdir, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// performing mounts
|
// performing mounts
|
||||||
for i, m := range r.Mounts {
|
for _, m := range r.Mounts {
|
||||||
// mkdirs(target)
|
// mkdirs(target)
|
||||||
for j, p := range m.Prefixes {
|
for i, p := range m.Prefixes {
|
||||||
// if target mount point is a file, mknod(target)
|
// if target mount point is a file, mknod(target)
|
||||||
if j == len(m.Prefixes)-1 && m.MakeNod {
|
if i == len(m.Prefixes)-1 && m.MakeNod {
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_MKNODAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(p)), 0755)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_MKNODAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(p)), 0755)
|
||||||
if err1 != 0 && err1 != syscall.EEXIST {
|
if err1 != 0 && err1 != syscall.EEXIST {
|
||||||
childExitErrorWithIndex(pipe, LocMountMkdir, i, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_MKDIRAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(p)), 0755)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_MKDIRAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(p)), 0755)
|
||||||
if err1 != 0 && err1 != syscall.EEXIST {
|
if err1 != 0 && err1 != syscall.EEXIST {
|
||||||
childExitErrorWithIndex(pipe, LocMountMkdir, i, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// mount(source, target, fsType, flags, data)
|
// mount(source, target, fsType, flags, data)
|
||||||
@ -257,52 +209,43 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
uintptr(unsafe.Pointer(m.Target)), uintptr(unsafe.Pointer(m.FsType)), uintptr(m.Flags),
|
uintptr(unsafe.Pointer(m.Target)), uintptr(unsafe.Pointer(m.FsType)), uintptr(m.Flags),
|
||||||
uintptr(unsafe.Pointer(m.Data)), 0)
|
uintptr(unsafe.Pointer(m.Data)), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitErrorWithIndex(pipe, LocMount, i, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
// bind mount is not respect ro flag so that read-only bind mount needs remount
|
// bind mount is not respect ro flag so that read-only bind mount needs remount
|
||||||
if m.Flags&bindRo == bindRo {
|
if m.Flags&bindRo == bindRo {
|
||||||
// Ensure the flag retains for bind mount
|
|
||||||
const mask = syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_NOEXEC | syscall.MS_NOATIME | syscall.MS_NODIRATIME | syscall.MS_RELATIME
|
|
||||||
var s syscall.Statfs_t
|
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_STATFS, uintptr(unsafe.Pointer(m.Source)), uintptr(unsafe.Pointer(&s)), 0)
|
|
||||||
if err1 != 0 {
|
|
||||||
childExitErrorWithIndex(pipe, LocMount, i, err1)
|
|
||||||
}
|
|
||||||
|
|
||||||
flag := m.Flags | syscall.MS_REMOUNT | uintptr(s.Flags&mask)
|
|
||||||
_, _, err1 = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(&empty[0])),
|
_, _, err1 = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(&empty[0])),
|
||||||
uintptr(unsafe.Pointer(m.Target)), uintptr(unsafe.Pointer(m.FsType)),
|
uintptr(unsafe.Pointer(m.Target)), uintptr(unsafe.Pointer(m.FsType)),
|
||||||
uintptr(flag), uintptr(unsafe.Pointer(m.Data)), 0)
|
uintptr(m.Flags|syscall.MS_REMOUNT), uintptr(unsafe.Pointer(m.Data)), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitErrorWithIndex(pipe, LocMount, i, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pivot_root
|
// pivit_root
|
||||||
if pivotRoot != nil {
|
if pivotRoot != nil {
|
||||||
// mkdir("old_root")
|
// mkdir("old_root")
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_MKDIRAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(&oldRoot[0])), 0755)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_MKDIRAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(&oldRoot[0])), 0755)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocPivotRoot, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// pivot_root(root, "old_root")
|
// pivot_root(root, "old_root")
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_PIVOT_ROOT, uintptr(unsafe.Pointer(pivotRoot)), uintptr(unsafe.Pointer(&oldRoot[0])), 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_PIVOT_ROOT, uintptr(unsafe.Pointer(pivotRoot)), uintptr(unsafe.Pointer(&oldRoot[0])), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocPivotRoot, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// umount("old_root", MNT_DETACH)
|
// umount("old_root", MNT_DETACH)
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_UMOUNT2, uintptr(unsafe.Pointer(&oldRoot[0])), syscall.MNT_DETACH, 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_UMOUNT2, uintptr(unsafe.Pointer(&oldRoot[0])), syscall.MNT_DETACH, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocPivotRoot, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// rmdir("old_root")
|
// rmdir("old_root")
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_UNLINKAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(&oldRoot[0])), uintptr(unix.AT_REMOVEDIR))
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_UNLINKAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(&oldRoot[0])), uintptr(unix.AT_REMOVEDIR))
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocPivotRoot, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
// mount("tmpfs", "/", "tmpfs", MS_BIND | MS_REMOUNT | MS_RDONLY | MS_NOATIME | MS_NOSUID, nil)
|
// mount("tmpfs", "/", "tmpfs", MS_BIND | MS_REMOUNT | MS_RDONLY | MS_NOATIME | MS_NOSUID, nil)
|
||||||
@ -311,20 +254,19 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
uintptr(syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY|syscall.MS_NOATIME|syscall.MS_NOSUID),
|
uintptr(syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY|syscall.MS_NOATIME|syscall.MS_NOSUID),
|
||||||
uintptr(unsafe.Pointer(&empty[0])), 0)
|
uintptr(unsafe.Pointer(&empty[0])), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocPivotRoot, err1)
|
goto childerror
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetHostName
|
// SetHostName
|
||||||
if hostname != nil {
|
if hostname != nil {
|
||||||
syscall.RawSyscall(syscall.SYS_SETHOSTNAME,
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_SETHOSTNAME,
|
||||||
uintptr(unsafe.Pointer(hostname)), uintptr(len(r.HostName)), 0)
|
uintptr(unsafe.Pointer(hostname)), uintptr(len(r.HostName)), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDomainName
|
// SetDomainName
|
||||||
if domainname != nil {
|
if domainname != nil {
|
||||||
syscall.RawSyscall(syscall.SYS_SETDOMAINNAME,
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_SETDOMAINNAME,
|
||||||
uintptr(unsafe.Pointer(domainname)), uintptr(len(r.DomainName)), 0)
|
uintptr(unsafe.Pointer(domainname)), uintptr(len(r.DomainName)), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,16 +274,16 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
if workdir != nil {
|
if workdir != nil {
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(workdir)), 0, 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(workdir)), 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocChdir, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set limit
|
// Set limit
|
||||||
for i, rlim = range r.RLimits {
|
for _, rlim := range r.RLimits {
|
||||||
// prlimit instead of setrlimit to avoid 32-bit limitation (linux > 3.2)
|
// 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)
|
_, _, err1 = syscall.RawSyscall6(syscall.SYS_PRLIMIT64, 0, uintptr(rlim.Res), uintptr(unsafe.Pointer(&rlim.Rlim)), 0, 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitErrorWithIndex(pipe, LocSetRlimit, i, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +291,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
if r.NoNewPrivs || r.Seccomp != nil {
|
if r.NoNewPrivs || r.Seccomp != nil {
|
||||||
_, _, err1 = syscall.RawSyscall6(syscall.SYS_PRCTL, unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0, 0)
|
_, _, err1 = syscall.RawSyscall6(syscall.SYS_PRCTL, unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSetNoNewPrivs, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,51 +301,50 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECUREBITS,
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECUREBITS,
|
||||||
_SECURE_KEEP_CAPS_LOCKED|_SECURE_NO_SETUID_FIXUP|_SECURE_NO_SETUID_FIXUP_LOCKED|_SECURE_NOROOT|_SECURE_NOROOT_LOCKED, 0)
|
_SECURE_KEEP_CAPS_LOCKED|_SECURE_NO_SETUID_FIXUP|_SECURE_NO_SETUID_FIXUP_LOCKED|_SECURE_NOROOT|_SECURE_NOROOT_LOCKED, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocDropCapability, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&dropCapHeader)), uintptr(unsafe.Pointer(&dropCapData)), 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&dropCapHeader)), uintptr(unsafe.Pointer(&dropCapData)), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSetCap, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable Ptrace & sync with parent (since ptrace_me is a blocking operation)
|
// Enable Ptrace & sync with parent (since ptrace_me is a blocking operation)
|
||||||
if r.Ptrace && r.Seccomp != nil {
|
if r.Ptrace && r.Seccomp != nil {
|
||||||
{
|
err2 = 0
|
||||||
if r.SyncFunc != nil {
|
|
||||||
r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
if r1 == 0 || err1 != 0 {
|
if r1 == 0 || err1 != 0 {
|
||||||
childExitError(pipe, LocSyncWrite, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
if r1 == 0 || err1 != 0 {
|
if r1 == 0 || err1 != 0 {
|
||||||
childExitError(pipe, LocSyncRead, err1)
|
goto childerror
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unshare cgroup namespace
|
// unshare cgroup namespace
|
||||||
if r.UnshareCgroupAfterSync {
|
if r.UnshareCgroupAfterSync {
|
||||||
// do not error if unshare fails, it is not critical
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_UNSHARE, uintptr(unix.CLONE_NEWCGROUP), 0, 0)
|
||||||
syscall.RawSyscall(syscall.SYS_UNSHARE, uintptr(unix.CLONE_NEWCGROUP), 0, 0)
|
if err1 != 0 {
|
||||||
|
goto childerror
|
||||||
|
}
|
||||||
if r.DropCaps || r.Credential != nil {
|
if r.DropCaps || r.Credential != nil {
|
||||||
// make sure the children have no privilege at all
|
// make sure the children have no privilege at all
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECUREBITS,
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECUREBITS,
|
||||||
_SECURE_KEEP_CAPS_LOCKED|_SECURE_NO_SETUID_FIXUP|_SECURE_NO_SETUID_FIXUP_LOCKED|_SECURE_NOROOT|_SECURE_NOROOT_LOCKED, 0)
|
_SECURE_KEEP_CAPS_LOCKED|_SECURE_NO_SETUID_FIXUP|_SECURE_NO_SETUID_FIXUP_LOCKED|_SECURE_NOROOT|_SECURE_NOROOT_LOCKED, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocKeepCapability, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&dropCapHeader)), uintptr(unsafe.Pointer(&dropCapData)), 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&dropCapHeader)), uintptr(unsafe.Pointer(&dropCapData)), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSetCap, err1)
|
goto childerror
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocPtraceMe, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +355,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
// Stop to wait for ptrace tracer
|
// Stop to wait for ptrace tracer
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_KILL, pid, uintptr(syscall.SIGSTOP), 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_KILL, pid, uintptr(syscall.SIGSTOP), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocStop, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,49 +369,46 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
// Load seccomp filter
|
// Load seccomp filter
|
||||||
_, _, err1 = syscall.RawSyscall(unix.SYS_SECCOMP, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, uintptr(unsafe.Pointer(r.Seccomp)))
|
_, _, err1 = syscall.RawSyscall(unix.SYS_SECCOMP, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, uintptr(unsafe.Pointer(r.Seccomp)))
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSeccomp, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before exec, sync with parent through pipe (configured as close_on_exec)
|
// Before exec, sync with parent through pipe (configured as close_on_exec)
|
||||||
if !r.Ptrace || r.Seccomp == nil {
|
if !r.Ptrace || r.Seccomp == nil {
|
||||||
{
|
err2 = 0
|
||||||
if r.SyncFunc != nil {
|
|
||||||
r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
if r1 == 0 || err1 != 0 {
|
if r1 == 0 || err1 != 0 {
|
||||||
childExitError(pipe, LocSyncWrite, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
|
|
||||||
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
if r1 == 0 || err1 != 0 {
|
if r1 == 0 || err1 != 0 {
|
||||||
childExitError(pipe, LocSyncRead, err1)
|
goto childerror
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unshare cgroup namespace
|
// unshare cgroup namespace
|
||||||
if r.UnshareCgroupAfterSync {
|
if r.UnshareCgroupAfterSync {
|
||||||
// do not error if unshare fails, it is not critical
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_UNSHARE, uintptr(unix.CLONE_NEWCGROUP), 0, 0)
|
||||||
syscall.RawSyscall(syscall.SYS_UNSHARE, uintptr(unix.CLONE_NEWCGROUP), 0, 0)
|
if err1 != 0 {
|
||||||
|
goto childerror
|
||||||
|
}
|
||||||
if r.DropCaps || r.Credential != nil {
|
if r.DropCaps || r.Credential != nil {
|
||||||
// make sure the children have no privilege at all
|
// make sure the children have no privilege at all
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECUREBITS,
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECUREBITS,
|
||||||
_SECURE_KEEP_CAPS_LOCKED|_SECURE_NO_SETUID_FIXUP|_SECURE_NO_SETUID_FIXUP_LOCKED|_SECURE_NOROOT|_SECURE_NOROOT_LOCKED, 0)
|
_SECURE_KEEP_CAPS_LOCKED|_SECURE_NO_SETUID_FIXUP|_SECURE_NO_SETUID_FIXUP_LOCKED|_SECURE_NOROOT|_SECURE_NOROOT_LOCKED, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocKeepCapability, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&dropCapHeader)), uintptr(unsafe.Pointer(&dropCapData)), 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&dropCapHeader)), uintptr(unsafe.Pointer(&dropCapData)), 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSetCap, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Seccomp != nil {
|
if r.Seccomp != nil {
|
||||||
// Load seccomp filter
|
// Load seccomp filter
|
||||||
_, _, err1 = syscall.RawSyscall(unix.SYS_SECCOMP, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, uintptr(unsafe.Pointer(r.Seccomp)))
|
_, _, err1 = syscall.RawSyscall(unix.SYS_SECCOMP, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, uintptr(unsafe.Pointer(r.Seccomp)))
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocSeccomp, err1)
|
goto childerror
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -480,7 +418,7 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
if r.Ptrace && r.Seccomp == nil {
|
if r.Ptrace && r.Seccomp == nil {
|
||||||
_, _, err1 = syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0)
|
_, _, err1 = syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0)
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
childExitError(pipe, LocPtraceMe, err1)
|
goto childerror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,63 +428,36 @@ func forkAndExecInChild(r *Runner, argv0 *byte, argv, env []*byte, workdir, host
|
|||||||
// if execfile fd is specified, call fexecve
|
// if execfile fd is specified, call fexecve
|
||||||
if r.ExecFile > 0 {
|
if r.ExecFile > 0 {
|
||||||
_, _, err1 = syscall.RawSyscall6(unix.SYS_EXECVEAT, r.ExecFile,
|
_, _, err1 = syscall.RawSyscall6(unix.SYS_EXECVEAT, r.ExecFile,
|
||||||
uintptr(unsafe.Pointer(&empty[0])), uintptr(unsafe.Pointer(&argv[0])),
|
uintptr(unsafe.Pointer(&empty[0])),
|
||||||
|
uintptr(unsafe.Pointer(&argv[0])),
|
||||||
uintptr(unsafe.Pointer(&env[0])), unix.AT_EMPTY_PATH, 0)
|
uintptr(unsafe.Pointer(&env[0])), unix.AT_EMPTY_PATH, 0)
|
||||||
} else {
|
} else {
|
||||||
_, _, err1 = syscall.RawSyscall(unix.SYS_EXECVE, uintptr(unsafe.Pointer(argv0)),
|
_, _, err1 = syscall.RawSyscall6(unix.SYS_EXECVEAT, uintptr(_AT_FDCWD),
|
||||||
uintptr(unsafe.Pointer(&argv[0])), uintptr(unsafe.Pointer(&env[0])))
|
uintptr(unsafe.Pointer(argv0)),
|
||||||
|
uintptr(unsafe.Pointer(&argv[0])),
|
||||||
|
uintptr(unsafe.Pointer(&env[0])), 0, 0)
|
||||||
}
|
}
|
||||||
// Fix potential ETXTBSY but with caution (max 50 attempt)
|
|
||||||
// The ETXTBSY happens when we copy the executable into container, another goroutine
|
// for slow devices, the file close is not as quickly as enough and it is causing ETXTBSY, retrying on this error
|
||||||
// forks but not execve yet (time consuming for setting up mounting points), the forked
|
for err1 == syscall.ETXTBSY {
|
||||||
// process is still holding the fd of the copied executable fd. However, we don't
|
|
||||||
// want to have different logic to lock the container creation
|
|
||||||
for range [50]struct{}{} {
|
|
||||||
if err1 != syscall.ETXTBSY {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// wait instead of busy wait
|
|
||||||
syscall.RawSyscall(unix.SYS_NANOSLEEP, uintptr(unsafe.Pointer(&etxtbsyRetryInterval)), 0, 0)
|
|
||||||
if r.ExecFile > 0 {
|
if r.ExecFile > 0 {
|
||||||
_, _, err1 = syscall.RawSyscall6(unix.SYS_EXECVEAT, r.ExecFile,
|
_, _, err1 = syscall.RawSyscall6(unix.SYS_EXECVEAT, r.ExecFile,
|
||||||
uintptr(unsafe.Pointer(&empty[0])), uintptr(unsafe.Pointer(&argv[0])),
|
uintptr(unsafe.Pointer(&empty[0])),
|
||||||
|
uintptr(unsafe.Pointer(&argv[0])),
|
||||||
uintptr(unsafe.Pointer(&env[0])), unix.AT_EMPTY_PATH, 0)
|
uintptr(unsafe.Pointer(&env[0])), unix.AT_EMPTY_PATH, 0)
|
||||||
} else {
|
} else {
|
||||||
_, _, err1 = syscall.RawSyscall(unix.SYS_EXECVE, uintptr(unsafe.Pointer(argv0)),
|
_, _, err1 = syscall.RawSyscall6(unix.SYS_EXECVEAT, uintptr(_AT_FDCWD),
|
||||||
uintptr(unsafe.Pointer(&argv[0])), uintptr(unsafe.Pointer(&env[0])))
|
uintptr(unsafe.Pointer(argv0)),
|
||||||
|
uintptr(unsafe.Pointer(&argv[0])),
|
||||||
|
uintptr(unsafe.Pointer(&env[0])), 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
childExitError(pipe, LocExecve, err1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:nosplit
|
|
||||||
func childExitError(pipe int, loc ErrorLocation, err syscall.Errno) {
|
|
||||||
// send error code on pipe
|
|
||||||
childError := ChildError{
|
|
||||||
Err: err,
|
|
||||||
Location: loc,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
childerror:
|
||||||
// send error code on pipe
|
// send error code on pipe
|
||||||
syscall.RawSyscall(unix.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&childError)), unsafe.Sizeof(childError))
|
syscall.RawSyscall(unix.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err1)), unsafe.Sizeof(err1))
|
||||||
for {
|
for {
|
||||||
syscall.RawSyscall(syscall.SYS_EXIT, uintptr(err), 0, 0)
|
syscall.RawSyscall(syscall.SYS_EXIT, uintptr(err1+err2), 0, 0)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:nosplit
|
|
||||||
func childExitErrorWithIndex(pipe int, loc ErrorLocation, idx int, err syscall.Errno) {
|
|
||||||
// send error code on pipe
|
|
||||||
childError := ChildError{
|
|
||||||
Err: err,
|
|
||||||
Location: loc,
|
|
||||||
Index: idx,
|
|
||||||
}
|
|
||||||
|
|
||||||
// send error code on pipe
|
|
||||||
syscall.RawSyscall(unix.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&childError)), unsafe.Sizeof(childError))
|
|
||||||
for {
|
|
||||||
syscall.RawSyscall(syscall.SYS_EXIT, uintptr(err), 0, 0)
|
|
||||||
}
|
}
|
||||||
|
// cannot reach this point
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package forkexec
|
package forkexec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ func syncWithChild(r *Runner, p [2]int, pid int, err1 syscall.Errno) (int, error
|
|||||||
unix.Close(p[0])
|
unix.Close(p[0])
|
||||||
return 0, syscall.Errno(err1)
|
return 0, syscall.Errno(err1)
|
||||||
}
|
}
|
||||||
r1, _, err1 = syscall3(libc_read_trampoline_addr, uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
r1, _, err1 = syscall3(funcPC(libc_read_trampoline), uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
// child returned error code
|
// child returned error code
|
||||||
if r1 != unsafe.Sizeof(err2) || err2 != 0 || err1 != 0 {
|
if r1 != unsafe.Sizeof(err2) || err2 != 0 || err1 != 0 {
|
||||||
err = handlePipeError(r1, err2)
|
err = handlePipeError(r1, err2)
|
||||||
@ -97,13 +98,13 @@ func syncWithChild(r *Runner, p [2]int, pid int, err1 syscall.Errno) (int, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// otherwise, ack child (err1 == 0)
|
// otherwise, ack child (err1 == 0)
|
||||||
r1, _, err1 = syscall3(libc_write_trampoline_addr, uintptr(p[0]), uintptr(unsafe.Pointer(&err1)), uintptr(unsafe.Sizeof(err1)))
|
r1, _, err1 = syscall3(funcPC(libc_write_trampoline), uintptr(p[0]), uintptr(unsafe.Pointer(&err1)), uintptr(unsafe.Sizeof(err1)))
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
// if read anything mean child failed after sync (close_on_exec so it should not block)
|
// if read anything mean child failed after sync (close_on_exec so it should not block)
|
||||||
r1, _, err1 = syscall3(libc_read_trampoline_addr, uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
r1, _, err1 = syscall3(funcPC(libc_read_trampoline), uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
unix.Close(p[0])
|
unix.Close(p[0])
|
||||||
if r1 != 0 || err1 != 0 {
|
if r1 != 0 || err1 != 0 {
|
||||||
err = handlePipeError(r1, err2)
|
err = handlePipeError(r1, err2)
|
||||||
@ -121,6 +122,7 @@ failAfterClose:
|
|||||||
|
|
||||||
// check pipe error
|
// check pipe error
|
||||||
func handlePipeError(r1 uintptr, errno syscall.Errno) error {
|
func handlePipeError(r1 uintptr, errno syscall.Errno) error {
|
||||||
|
log.Println(r1, errno, int(errno))
|
||||||
if r1 == unsafe.Sizeof(errno) {
|
if r1 == unsafe.Sizeof(errno) {
|
||||||
return syscall.Errno(errno)
|
return syscall.Errno(errno)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,11 +61,10 @@ func (r *Runner) Start() (int, error) {
|
|||||||
|
|
||||||
func syncWithChild(r *Runner, p [2]int, pid int, err1 syscall.Errno) (int, error) {
|
func syncWithChild(r *Runner, p [2]int, pid int, err1 syscall.Errno) (int, error) {
|
||||||
var (
|
var (
|
||||||
|
r1 uintptr
|
||||||
err2 syscall.Errno
|
err2 syscall.Errno
|
||||||
err error
|
err error
|
||||||
unshareUser = r.CloneFlags&unix.CLONE_NEWUSER == unix.CLONE_NEWUSER
|
unshareUser = r.CloneFlags&unix.CLONE_NEWUSER == unix.CLONE_NEWUSER
|
||||||
childErr ChildError
|
|
||||||
n int
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// sync with child
|
// sync with child
|
||||||
@ -74,9 +73,7 @@ func syncWithChild(r *Runner, p [2]int, pid int, err1 syscall.Errno) (int, error
|
|||||||
// clone syscall failed
|
// clone syscall failed
|
||||||
if err1 != 0 {
|
if err1 != 0 {
|
||||||
unix.Close(p[0])
|
unix.Close(p[0])
|
||||||
childErr.Location = LocClone
|
return 0, syscall.Errno(err1)
|
||||||
childErr.Err = err1
|
|
||||||
return 0, childErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// synchronize with child for uid / gid map
|
// synchronize with child for uid / gid map
|
||||||
@ -87,37 +84,33 @@ 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)))
|
syscall.RawSyscall(syscall.SYS_WRITE, uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if syncfunc return error, then fail child immediately
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
// only sync if there is a syncFunc
|
|
||||||
if r.SyncFunc != nil {
|
|
||||||
n, err = readChildErr(p[0], &childErr)
|
|
||||||
// child returned error code
|
// child returned error code
|
||||||
if (n != int(unsafe.Sizeof(err2)) && n != int(unsafe.Sizeof(childErr))) || childErr.Err != 0 || err != nil {
|
if r1 != unsafe.Sizeof(err2) || err2 != 0 || err1 != 0 {
|
||||||
childErr.Err = handlePipeError(n, childErr.Err)
|
err = handlePipeError(r1, err2)
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if syncfunc return error, then fail child immediately
|
||||||
|
if r.SyncFunc != nil {
|
||||||
if err = r.SyncFunc(int(pid)); err != nil {
|
if err = r.SyncFunc(int(pid)); err != nil {
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// otherwise, ack child (err1 == 0)
|
// otherwise, ack child (err1 == 0)
|
||||||
syscall.RawSyscall(syscall.SYS_WRITE, uintptr(p[0]), uintptr(unsafe.Pointer(&err1)), uintptr(unsafe.Sizeof(err1)))
|
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 stopped before execve by signal SIGSTOP or PTRACE_ME, then do not wait until execve
|
||||||
if r.StopBeforeSeccomp || (r.Seccomp != nil && r.Ptrace) {
|
if r.Ptrace || r.StopBeforeSeccomp {
|
||||||
// let's wait it in another goroutine to avoid SIGPIPE
|
|
||||||
go func() {
|
|
||||||
readChildErr(p[0], &childErr)
|
|
||||||
unix.Close(p[0])
|
unix.Close(p[0])
|
||||||
}()
|
|
||||||
return int(pid), nil
|
return int(pid), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if read anything mean child failed after sync (close_on_exec so it should not block)
|
// if read anything mean child failed after sync (close_on_exec so it should not block)
|
||||||
n, err = readChildErr(p[0], &childErr)
|
r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2)))
|
||||||
unix.Close(p[0])
|
unix.Close(p[0])
|
||||||
if n != 0 || err != nil {
|
if r1 != 0 || err1 != 0 {
|
||||||
childErr.Err = handlePipeError(n, childErr.Err)
|
err = handlePipeError(r1, err2)
|
||||||
goto failAfterClose
|
goto failAfterClose
|
||||||
}
|
}
|
||||||
return int(pid), nil
|
return int(pid), nil
|
||||||
@ -127,35 +120,12 @@ fail:
|
|||||||
|
|
||||||
failAfterClose:
|
failAfterClose:
|
||||||
handleChildFailed(int(pid))
|
handleChildFailed(int(pid))
|
||||||
if childErr.Err == 0 {
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
|
||||||
return 0, childErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func readChildErr(fd int, childErr *ChildError) (n int, err error) {
|
|
||||||
for {
|
|
||||||
n, err = readlen(fd, (*byte)(unsafe.Pointer(childErr)), int(unsafe.Sizeof(*childErr)))
|
|
||||||
if err != syscall.EINTR {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://cs.opensource.google/go/go/+/refs/tags/go1.18.1:src/syscall/zsyscall_linux_amd64.go;l=944
|
|
||||||
func readlen(fd int, p *byte, np int) (n int, err error) {
|
|
||||||
r0, _, e1 := syscall.Syscall(syscall.SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(np))
|
|
||||||
n = int(r0)
|
|
||||||
if e1 != 0 {
|
|
||||||
err = syscall.Errno(e1)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check pipe error
|
// check pipe error
|
||||||
func handlePipeError(r1 int, errno syscall.Errno) syscall.Errno {
|
func handlePipeError(r1 uintptr, errno syscall.Errno) error {
|
||||||
if uintptr(r1) >= unsafe.Sizeof(errno) {
|
if r1 == unsafe.Sizeof(errno) {
|
||||||
return syscall.Errno(errno)
|
return syscall.Errno(errno)
|
||||||
}
|
}
|
||||||
return syscall.EPIPE
|
return syscall.EPIPE
|
||||||
|
|||||||
@ -1,119 +0,0 @@
|
|||||||
package forkexec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/mount"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFork_DropCaps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
r := Runner{
|
|
||||||
Args: []string{"/bin/echo"},
|
|
||||||
CloneFlags: syscall.CLONE_NEWUSER,
|
|
||||||
DropCaps: true,
|
|
||||||
}
|
|
||||||
_, err := r.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFork_ETXTBSY(t *testing.T) {
|
|
||||||
f, err := os.CreateTemp("", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
os.Remove(f.Name())
|
|
||||||
f.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := f.Chmod(0777); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
echo, err := os.Open("/bin/echo")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer echo.Close()
|
|
||||||
|
|
||||||
_, err = f.ReadFrom(echo)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := Runner{
|
|
||||||
Args: []string{f.Name()},
|
|
||||||
ExecFile: f.Fd(),
|
|
||||||
}
|
|
||||||
_, err = r.Start()
|
|
||||||
e, ok := err.(ChildError)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("not a child error")
|
|
||||||
}
|
|
||||||
if e.Err != syscall.ETXTBSY && e.Location != LocExecve && e.Index != 0 {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFork_OK(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
f, err := os.CreateTemp("", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
|
|
||||||
if err := f.Chmod(0777); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
echo, err := os.Open("/bin/echo")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer echo.Close()
|
|
||||||
|
|
||||||
_, err = f.ReadFrom(echo)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
r := Runner{
|
|
||||||
Args: []string{f.Name()},
|
|
||||||
}
|
|
||||||
_, err = r.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFork_ENOENT(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
m, err := mount.NewBuilder().
|
|
||||||
WithMount(
|
|
||||||
mount.Mount{
|
|
||||||
Source: "NOT_EXISTS",
|
|
||||||
}).Build()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
r := Runner{
|
|
||||||
Args: []string{"/bin/echo"},
|
|
||||||
CloneFlags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
|
|
||||||
Mounts: m,
|
|
||||||
}
|
|
||||||
_, err = r.Start()
|
|
||||||
e, ok := err.(ChildError)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("not a child error")
|
|
||||||
}
|
|
||||||
if e.Err != syscall.ENOENT && e.Location != LocExecve {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,7 +14,7 @@ type Runner struct {
|
|||||||
// POSIX Resource limit set by set rlimit
|
// POSIX Resource limit set by set rlimit
|
||||||
RLimits []rlimit.RLimit
|
RLimits []rlimit.RLimit
|
||||||
|
|
||||||
// file descriptors map for new process, from 0 to len - 1
|
// file disriptors map for new process, from 0 to len - 1
|
||||||
Files []uintptr
|
Files []uintptr
|
||||||
|
|
||||||
// work path set by chdir(dir) (current working directory for child)
|
// work path set by chdir(dir) (current working directory for child)
|
||||||
@ -24,7 +24,7 @@ type Runner struct {
|
|||||||
// sandbox profile defines the sandbox profile for sandbox_init syscall
|
// sandbox profile defines the sandbox profile for sandbox_init syscall
|
||||||
SandboxProfile string
|
SandboxProfile string
|
||||||
|
|
||||||
// Parent and child process with sync status through a socket pair.
|
// Parent and child process with sync sataus through a socket pair.
|
||||||
// SyncFunc will invoke with the child pid. If SyncFunc return some error,
|
// SyncFunc will invoke with the child pid. If SyncFunc return some error,
|
||||||
// parent will signal child to stop and report the error
|
// parent will signal child to stop and report the error
|
||||||
// SyncFunc is called right before execve, thus it could track cpu more accurately
|
// SyncFunc is called right before execve, thus it could track cpu more accurately
|
||||||
|
|||||||
@ -21,7 +21,7 @@ type Runner struct {
|
|||||||
// POSIX Resource limit set by set rlimit
|
// POSIX Resource limit set by set rlimit
|
||||||
RLimits []rlimit.RLimit
|
RLimits []rlimit.RLimit
|
||||||
|
|
||||||
// file descriptors map for new process, from 0 to len - 1
|
// file disriptors map for new process, from 0 to len - 1
|
||||||
Files []uintptr
|
Files []uintptr
|
||||||
|
|
||||||
// work path set by chdir(dir) (current working directory for child)
|
// work path set by chdir(dir) (current working directory for child)
|
||||||
@ -31,6 +31,21 @@ type Runner struct {
|
|||||||
// seccomp syscall filter applied to child
|
// seccomp syscall filter applied to child
|
||||||
Seccomp *syscall.SockFprog
|
Seccomp *syscall.SockFprog
|
||||||
|
|
||||||
|
// ptrace controls child process to call ptrace(PTRACE_TRACEME)
|
||||||
|
// runtime.LockOSThread is required for tracer to call ptrace syscalls
|
||||||
|
Ptrace bool
|
||||||
|
|
||||||
|
// no_new_privs calls prctl(PR_SET_NO_NEW_PRIVS) to 0 to disable calls to
|
||||||
|
// setuid processes. It is automatically enabled when seccomp filter is provided
|
||||||
|
NoNewPrivs bool
|
||||||
|
|
||||||
|
// stop before seccomp calls kill(getpid(), SIGSTOP) to wait for tracer to continue
|
||||||
|
// right before the calls to seccomp. It is automatically enabled when seccomp
|
||||||
|
// filter and ptrace are provided since kill might not be available after
|
||||||
|
// seccomp and execve might be traced by ptrace
|
||||||
|
// cannot stop after seccomp since kill might not be allowed by seccomp filter
|
||||||
|
StopBeforeSeccomp bool
|
||||||
|
|
||||||
// clone unshare flag to create linux namespace, effective when clone child
|
// clone unshare flag to create linux namespace, effective when clone child
|
||||||
// since unshare syscall does not join the new pid group
|
// since unshare syscall does not join the new pid group
|
||||||
CloneFlags uintptr
|
CloneFlags uintptr
|
||||||
@ -57,51 +72,30 @@ type Runner struct {
|
|||||||
// HostName and DomainName to be set after unshare UTS & user (CAP_SYS_ADMIN)
|
// HostName and DomainName to be set after unshare UTS & user (CAP_SYS_ADMIN)
|
||||||
HostName, DomainName string
|
HostName, DomainName string
|
||||||
|
|
||||||
// UidMappings / GidMappings for unshared user namespaces, no-op if mapping is null
|
|
||||||
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
|
|
||||||
|
|
||||||
// Parent and child process with sync status through a socket pair.
|
|
||||||
// SyncFunc will invoke with the child pid. If SyncFunc return some error,
|
|
||||||
// parent will signal child to stop and report the error
|
|
||||||
// SyncFunc is called right before execve, thus it could track cpu more accurately
|
|
||||||
SyncFunc func(int) error
|
|
||||||
|
|
||||||
// ptrace controls child process to call ptrace(PTRACE_TRACEME)
|
|
||||||
// runtime.LockOSThread is required for tracer to call ptrace syscalls
|
|
||||||
Ptrace bool
|
|
||||||
|
|
||||||
// no_new_privs calls prctl(PR_SET_NO_NEW_PRIVS) to 0 to disable calls to
|
|
||||||
// setuid processes. It is automatically enabled when seccomp filter is provided
|
|
||||||
NoNewPrivs bool
|
|
||||||
|
|
||||||
// stop before seccomp calls kill(getpid(), SIGSTOP) to wait for tracer to continue
|
|
||||||
// right before the calls to seccomp. It is automatically enabled when seccomp
|
|
||||||
// filter and ptrace are provided since kill might not be available after
|
|
||||||
// seccomp and execve might be traced by ptrace
|
|
||||||
// cannot stop after seccomp since kill might not be allowed by seccomp filter
|
|
||||||
StopBeforeSeccomp bool
|
|
||||||
|
|
||||||
// GidMappingsEnableSetgroups allows / disallows setgroups syscall.
|
|
||||||
// deny if GIDMappings is nil
|
|
||||||
GIDMappingsEnableSetgroups bool
|
|
||||||
|
|
||||||
// drop_caps calls cap_set(self, 0) to drop all capabilities
|
// drop_caps calls cap_set(self, 0) to drop all capabilities
|
||||||
// from effective, permitted, inheritable capability sets before execve
|
// from effective, permitted, inheritable capability sets before execve
|
||||||
// it should avoid calls to set ambient capabilities
|
// it should avoid calls to set ambient capabilities
|
||||||
DropCaps bool
|
DropCaps bool
|
||||||
|
|
||||||
|
// UidMappings / GidMappings for unshared user namespaces, no-op if mapping is null
|
||||||
|
UIDMappings []syscall.SysProcIDMap
|
||||||
|
GIDMappings []syscall.SysProcIDMap
|
||||||
|
|
||||||
|
// GidMappingsEnableSetgroups allows / disallows setgroups syscall.
|
||||||
|
// deny if GIDMappings is nil
|
||||||
|
GIDMappingsEnableSetgroups bool
|
||||||
|
|
||||||
|
// Credential holds user and group identities to be assumed
|
||||||
|
// by a child process started by StartProcess.
|
||||||
|
Credential *syscall.Credential
|
||||||
|
|
||||||
|
// Parent and child process with sync sataus through a socket pair.
|
||||||
|
// SyncFunc will invoke with the child pid. If SyncFunc return some error,
|
||||||
|
// parent will signal child to stop and report the error
|
||||||
|
// SyncFunc is called right before execve, thus it could track cpu more accurately
|
||||||
|
SyncFunc func(int) error
|
||||||
|
|
||||||
// UnshareCgroupAfterSync specifies whether to unshare cgroup namespace after
|
// UnshareCgroupAfterSync specifies whether to unshare cgroup namespace after
|
||||||
// sync (the syncFunc might be add the child to the cgroup)
|
// sync (the syncFunc might be add the child to the cgroup)
|
||||||
UnshareCgroupAfterSync bool
|
UnshareCgroupAfterSync bool
|
||||||
|
|
||||||
// CTTY specifies if set the fd 0 as controlling TTY
|
|
||||||
CTTY bool
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,15 @@ package forkexec
|
|||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
||||||
_ "unsafe" // use go:linkname
|
_ "unsafe" // use go:linkname
|
||||||
|
|
||||||
|
"github.com/criyle/go-sandbox/pkg/darwin" // use sandbox_init
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ = darwin.SandboxInit
|
||||||
|
|
||||||
|
//go:linkname funcPC syscall.funcPC
|
||||||
|
func funcPC(f func()) uintptr
|
||||||
|
|
||||||
//go:linkname syscall3 syscall.syscall
|
//go:linkname syscall3 syscall.syscall
|
||||||
func syscall3(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
func syscall3(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
||||||
|
|
||||||
@ -14,27 +21,41 @@ func rawSyscall(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
|||||||
//go:linkname rawSyscall6 syscall.rawSyscall6
|
//go:linkname rawSyscall6 syscall.rawSyscall6
|
||||||
func rawSyscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
func rawSyscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
||||||
|
|
||||||
var libc_fork_trampoline_addr uintptr
|
//go:linkname libc_fork_trampoline syscall.libc_fork_trampoline
|
||||||
|
func libc_fork_trampoline()
|
||||||
|
|
||||||
var libc_close_trampoline_addr uintptr
|
//go:linkname libc_close_trampoline syscall.libc_close_trampoline
|
||||||
|
func libc_close_trampoline()
|
||||||
|
|
||||||
var libc_read_trampoline_addr uintptr
|
//go:linkname libc_read_trampoline syscall.libc_read_trampoline
|
||||||
|
func libc_read_trampoline()
|
||||||
|
|
||||||
var libc_write_trampoline_addr uintptr
|
//go:linkname libc_write_trampoline syscall.libc_write_trampoline
|
||||||
|
func libc_write_trampoline()
|
||||||
|
|
||||||
var libc_fcntl_trampoline_addr uintptr
|
//go:linkname libc_fcntl_trampoline syscall.libc_fcntl_trampoline
|
||||||
|
func libc_fcntl_trampoline()
|
||||||
|
|
||||||
var libc_dup2_trampoline_addr uintptr
|
//go:linkname libc_dup2_trampoline syscall.libc_dup2_trampoline
|
||||||
|
func libc_dup2_trampoline()
|
||||||
|
|
||||||
var libc_chdir_trampoline_addr uintptr
|
//go:linkname libc_chdir_trampoline syscall.libc_chdir_trampoline
|
||||||
|
func libc_chdir_trampoline()
|
||||||
|
|
||||||
var libc_setrlimit_trampoline_addr uintptr
|
//go:linkname libc_setrlimit_trampoline syscall.libc_setrlimit_trampoline
|
||||||
|
func libc_setrlimit_trampoline()
|
||||||
|
|
||||||
var libc_execve_trampoline_addr uintptr
|
//go:linkname libc_execve_trampoline syscall.libc_execve_trampoline
|
||||||
|
func libc_execve_trampoline()
|
||||||
|
|
||||||
var libc_exit_trampoline_addr uintptr
|
//go:linkname libc_exit_trampoline syscall.libc_exit_trampoline
|
||||||
|
func libc_exit_trampoline()
|
||||||
|
|
||||||
var libc_setpgid_trampoline_addr uintptr
|
//go:linkname libc_sandbox_init_trampoline github.com/criyle/go-sandbox/pkg/darwin.libc_sandbox_init_trampoline
|
||||||
|
func libc_sandbox_init_trampoline()
|
||||||
|
|
||||||
|
//go:linkname libc_sandbox_free_error_trampoline github.com/criyle/go-sandbox/pkg/darwin.libc_sandbox_free_error_trampoline
|
||||||
|
func libc_sandbox_free_error_trampoline()
|
||||||
|
|
||||||
//go:linkname fcntl syscall.fcntl
|
//go:linkname fcntl syscall.fcntl
|
||||||
func fcntl(fd int, cmd int, arg int) (val int, err error)
|
func fcntl(fd int, cmd int, arg int) (val int, err error)
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
// 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)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package forkexec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SandboxInit calls sandbox_init
|
|
||||||
func SandboxInit(profile *byte, flags uint64, errorBuf **byte) (err error) {
|
|
||||||
var r1 uintptr
|
|
||||||
r1, _, err = syscall3(libc_sandbox_init_trampoline_addr, uintptr(unsafe.Pointer(profile)), uintptr(flags), uintptr(unsafe.Pointer(errorBuf)))
|
|
||||||
if r1 != 0 {
|
|
||||||
err = syscall.EINVAL
|
|
||||||
} else {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SandboxFreeError calls sandbox_free_error
|
|
||||||
func SandboxFreeError(errorBuf *byte) {
|
|
||||||
syscall3(libc_sandbox_free_error_trampoline_addr, uintptr(unsafe.Pointer(errorBuf)), 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var libc_sandbox_init_trampoline_addr uintptr
|
|
||||||
|
|
||||||
//go:cgo_import_dynamic libc_sandbox_init sandbox_init "/usr/lib/libSystem.B.dylib"
|
|
||||||
|
|
||||||
var libc_sandbox_free_error_trampoline_addr uintptr
|
|
||||||
|
|
||||||
//go:cgo_import_dynamic libc_sandbox_free_error sandbox_free_error "/usr/lib/libSystem.B.dylib"
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
TEXT libc_sandbox_init_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_sandbox_init(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_sandbox_init_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_sandbox_init_trampoline_addr(SB)/8, $libc_sandbox_init_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_sandbox_free_error_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_sandbox_free_error(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_sandbox_free_error_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_sandbox_free_error_trampoline_addr(SB)/8, $libc_sandbox_free_error_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_fork_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_fork(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_fork_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_fork_trampoline_addr(SB)/8, $libc_fork_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_close_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_close(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_close_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_close_trampoline_addr(SB)/8, $libc_close_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_read_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_read(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_read_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_read_trampoline_addr(SB)/8, $libc_read_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_write_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_write(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_write_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_write_trampoline_addr(SB)/8, $libc_write_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_fcntl_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_fcntl(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_fcntl_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_fcntl_trampoline_addr(SB)/8, $libc_fcntl_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_dup2_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_dup2(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_dup2_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_dup2_trampoline_addr(SB)/8, $libc_dup2_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_chdir_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_chdir(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_chdir_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_chdir_trampoline_addr(SB)/8, $libc_chdir_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_setrlimit_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_setrlimit(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_setrlimit_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_setrlimit_trampoline_addr(SB)/8, $libc_setrlimit_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_execve_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_execve(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_execve_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_execve_trampoline_addr(SB)/8, $libc_execve_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_exit_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_exit(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_exit_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_exit_trampoline_addr(SB)/8, $libc_exit_trampoline<>(SB)
|
|
||||||
|
|
||||||
TEXT libc_setpgid_trampoline<>(SB),NOSPLIT,$0-0
|
|
||||||
JMP libc_setpgid(SB)
|
|
||||||
|
|
||||||
GLOBL ·libc_setpgid_trampoline_addr(SB), RODATA, $8
|
|
||||||
DATA ·libc_setpgid_trampoline_addr(SB)/8, $libc_setpgid_trampoline<>(SB)
|
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
package memfd
|
package memfd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -15,12 +17,12 @@ const roSeal = unix.F_SEAL_SEAL | unix.F_SEAL_SHRINK | unix.F_SEAL_GROW | unix.F
|
|||||||
func New(name string) (*os.File, error) {
|
func New(name string) (*os.File, error) {
|
||||||
fd, err := unix.MemfdCreate(name, createFlag)
|
fd, err := unix.MemfdCreate(name, createFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("memfd: memfd_create: %w", err)
|
return nil, fmt.Errorf("memfd: memfd_create failed %v", err)
|
||||||
}
|
}
|
||||||
file := os.NewFile(uintptr(fd), name)
|
file := os.NewFile(uintptr(fd), name)
|
||||||
if file == nil {
|
if file == nil {
|
||||||
unix.Close(fd)
|
unix.Close(fd)
|
||||||
return nil, fmt.Errorf("memfd: new file failed for %q", name)
|
return nil, fmt.Errorf("memfd: NewFile failed for %v", name)
|
||||||
}
|
}
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
@ -29,21 +31,21 @@ func New(name string) (*os.File, error) {
|
|||||||
func DupToMemfd(name string, reader io.Reader) (*os.File, error) {
|
func DupToMemfd(name string, reader io.Reader) (*os.File, error) {
|
||||||
file, err := New(name)
|
file, err := New(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("memfd: dup: %w", err)
|
return nil, fmt.Errorf("DupToMemfd: %v", err)
|
||||||
}
|
}
|
||||||
// linux syscall sendfile might be more efficient here if reader is a file
|
// linux syscall sendfile might be more efficient here if reader is a file
|
||||||
if _, err = file.ReadFrom(reader); err != nil {
|
if _, err = io.Copy(file, reader); err != nil {
|
||||||
file.Close()
|
file.Close()
|
||||||
return nil, fmt.Errorf("memfd: read from: %w", err)
|
return nil, fmt.Errorf("DupToMemfd: io.Copy %v", err)
|
||||||
}
|
}
|
||||||
// make memfd readonly
|
// make memfd readonly
|
||||||
if _, err = unix.FcntlInt(file.Fd(), unix.F_ADD_SEALS, roSeal); err != nil {
|
if _, err = unix.FcntlInt(file.Fd(), unix.F_ADD_SEALS, roSeal); err != nil {
|
||||||
file.Close()
|
file.Close()
|
||||||
return nil, fmt.Errorf("memfd: seal: %w", err)
|
return nil, fmt.Errorf("DupToMemfd: memfd seal %v", err)
|
||||||
}
|
}
|
||||||
if _, err := file.Seek(0, 0); err != nil {
|
if _, err := file.Seek(0, 0); err != nil {
|
||||||
file.Close()
|
file.Close()
|
||||||
return nil, fmt.Errorf("memfd: seek: %w", err)
|
return nil, fmt.Errorf("DupToMemfd: file seek %v", err)
|
||||||
}
|
}
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
package memfd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
f, err := New("test-memfd")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("New() error: %v", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
// Write and read to verify it's a valid file
|
|
||||||
data := []byte("hello world")
|
|
||||||
n, err := f.Write(data)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Write error: %v", err)
|
|
||||||
}
|
|
||||||
if n != len(data) {
|
|
||||||
t.Errorf("Write n = %d, want %d", n, len(data))
|
|
||||||
}
|
|
||||||
_, err = f.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Seek error: %v", err)
|
|
||||||
}
|
|
||||||
read := make([]byte, len(data))
|
|
||||||
n, err = f.Read(read)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
t.Fatalf("Read error: %v", err)
|
|
||||||
}
|
|
||||||
if string(read[:n]) != string(data) {
|
|
||||||
t.Errorf("Read = %q, want %q", string(read[:n]), string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDupToMemfd(t *testing.T) {
|
|
||||||
content := []byte("memfd content")
|
|
||||||
r := bytes.NewReader(content)
|
|
||||||
f, err := DupToMemfd("dup-memfd", r)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("DupToMemfd error: %v", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
// Should be sealed (readonly), so writing should fail
|
|
||||||
_, err = f.Write([]byte("fail"))
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected write to sealed memfd to fail, but it succeeded")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be able to read the content
|
|
||||||
_, err = f.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Seek error: %v", err)
|
|
||||||
}
|
|
||||||
got, err := io.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReadAll error: %v", err)
|
|
||||||
}
|
|
||||||
if string(got) != string(content) {
|
|
||||||
t.Errorf("ReadAll = %q, want %q", string(got), string(content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDupToMemfd_ErrorPropagation(t *testing.T) {
|
|
||||||
// Pass a reader that always errors
|
|
||||||
r := errorReader{}
|
|
||||||
_, err := DupToMemfd("dup-memfd-err", r)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error from DupToMemfd, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type errorReader struct{}
|
|
||||||
|
|
||||||
func (errorReader) Read([]byte) (int, error) { return 0, os.ErrInvalid }
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
//go:build !linux
|
// +build !linux
|
||||||
|
|
||||||
package memfd
|
package memfd
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errNotImplemented = fmt.Errorf("memfd: unsupported on platform: %s", runtime.GOOS)
|
var errNotImplemented = fmt.Errorf("memfd: unsupported on platform %s", runtime.GOOS)
|
||||||
|
|
||||||
func New(name string) (*os.File, error) {
|
func New(name string) (*os.File, error) {
|
||||||
return nil, errNotImplemented
|
return nil, errNotImplemented
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
bind = unix.MS_BIND | unix.MS_NOSUID | unix.MS_PRIVATE | unix.MS_REC
|
bind = unix.MS_BIND | unix.MS_NOSUID | unix.MS_PRIVATE
|
||||||
mFlag = unix.MS_NOSUID | unix.MS_NOATIME | unix.MS_NODEV
|
mFlag = unix.MS_NOSUID | unix.MS_NOATIME | unix.MS_NODEV
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,12 +22,16 @@ func NewDefaultBuilder() *Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build creates sequence of syscalls for fork_exec
|
// Build creates sequence of syscalls for fork_exec
|
||||||
func (b *Builder) Build() ([]SyscallParams, error) {
|
// skipNotExists skips bind mounts that source not exists
|
||||||
|
func (b *Builder) Build(skipNotExists bool) ([]SyscallParams, error) {
|
||||||
var err error
|
var err error
|
||||||
ret := make([]SyscallParams, 0, len(b.Mounts))
|
ret := make([]SyscallParams, 0, len(b.Mounts))
|
||||||
for _, m := range b.Mounts {
|
for _, m := range b.Mounts {
|
||||||
var mknod bool
|
var mknod bool
|
||||||
if mknod, err = isBindMountFileOrNotExists(m); err != nil {
|
if mknod, err = isBindMountFileOrNotExists(m); err != nil {
|
||||||
|
if skipNotExists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sp, err := m.ToSyscall()
|
sp, err := m.ToSyscall()
|
||||||
@ -40,23 +44,8 @@ func (b *Builder) Build() ([]SyscallParams, error) {
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterNotExist removes bind mount that does not exists
|
|
||||||
func (b *Builder) FilterNotExist() *Builder {
|
|
||||||
rt := b.Mounts[:0]
|
|
||||||
for _, m := range b.Mounts {
|
|
||||||
if m.IsBindMount() {
|
|
||||||
if _, err := os.Stat(m.Source); os.IsNotExist(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rt = append(rt, m)
|
|
||||||
}
|
|
||||||
b.Mounts = rt
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBindMountFileOrNotExists(m Mount) (bool, error) {
|
func isBindMountFileOrNotExists(m Mount) (bool, error) {
|
||||||
if m.IsBindMount() {
|
if m.Flags&unix.MS_BIND == unix.MS_BIND {
|
||||||
if fi, err := os.Stat(m.Source); os.IsNotExist(err) {
|
if fi, err := os.Stat(m.Source); os.IsNotExist(err) {
|
||||||
return false, err
|
return false, err
|
||||||
} else if !fi.IsDir() {
|
} else if !fi.IsDir() {
|
||||||
@ -66,13 +55,13 @@ func isBindMountFileOrNotExists(m Mount) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMounts adds mounts to builder
|
// WithMounts add mounts to builder
|
||||||
func (b *Builder) WithMounts(m []Mount) *Builder {
|
func (b *Builder) WithMounts(m []Mount) *Builder {
|
||||||
b.Mounts = append(b.Mounts, m...)
|
b.Mounts = append(b.Mounts, m...)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMount adds single mount to builder
|
// WithMount add single mount to builder
|
||||||
func (b *Builder) WithMount(m Mount) *Builder {
|
func (b *Builder) WithMount(m Mount) *Builder {
|
||||||
b.Mounts = append(b.Mounts, m)
|
b.Mounts = append(b.Mounts, m)
|
||||||
return b
|
return b
|
||||||
@ -92,7 +81,7 @@ func (b *Builder) WithBind(source, target string, readonly bool) *Builder {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithTmpfs adds a tmpfs mount to builder
|
// WithTmpfs add a tmpfs mount to builder
|
||||||
func (b *Builder) WithTmpfs(target, data string) *Builder {
|
func (b *Builder) WithTmpfs(target, data string) *Builder {
|
||||||
b.Mounts = append(b.Mounts, Mount{
|
b.Mounts = append(b.Mounts, Mount{
|
||||||
Source: "tmpfs",
|
Source: "tmpfs",
|
||||||
@ -104,22 +93,13 @@ func (b *Builder) WithTmpfs(target, data string) *Builder {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithProc adds proc file system mounted read-only
|
// WithProc add proc file system
|
||||||
func (b *Builder) WithProc() *Builder {
|
func (b *Builder) WithProc() *Builder {
|
||||||
return b.WithProcRW(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProcRW adds proc file system, possibly read-write
|
|
||||||
func (b *Builder) WithProcRW(canWrite bool) *Builder {
|
|
||||||
var flags uintptr = unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC
|
|
||||||
if !canWrite {
|
|
||||||
flags |= unix.MS_RDONLY
|
|
||||||
}
|
|
||||||
b.Mounts = append(b.Mounts, Mount{
|
b.Mounts = append(b.Mounts, Mount{
|
||||||
Source: "proc",
|
Source: "proc",
|
||||||
Target: "proc",
|
Target: "proc",
|
||||||
FsType: "proc",
|
FsType: "proc",
|
||||||
Flags: flags,
|
Flags: unix.MS_NOSUID | unix.MS_RDONLY,
|
||||||
})
|
})
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
package mount
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuilder_WithBind(t *testing.T) {
|
|
||||||
b := NewBuilder().WithBind("/src", "/dst", true)
|
|
||||||
if len(b.Mounts) != 1 {
|
|
||||||
t.Fatalf("expected 1 mount, got %d", len(b.Mounts))
|
|
||||||
}
|
|
||||||
m := b.Mounts[0]
|
|
||||||
if m.Source != "/src" || m.Target != "/dst" {
|
|
||||||
t.Errorf("unexpected mount: %+v", m)
|
|
||||||
}
|
|
||||||
if !m.IsBindMount() {
|
|
||||||
t.Errorf("expected bind mount")
|
|
||||||
}
|
|
||||||
if !m.IsReadOnly() {
|
|
||||||
t.Errorf("expected readonly mount")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuilder_WithTmpfs(t *testing.T) {
|
|
||||||
b := NewBuilder().WithTmpfs("/tmp", "size=64m")
|
|
||||||
if len(b.Mounts) != 1 {
|
|
||||||
t.Fatalf("expected 1 mount, got %d", len(b.Mounts))
|
|
||||||
}
|
|
||||||
m := b.Mounts[0]
|
|
||||||
if !m.IsTmpFs() {
|
|
||||||
t.Errorf("expected tmpfs mount")
|
|
||||||
}
|
|
||||||
if m.Target != "/tmp" || m.Data != "size=64m" {
|
|
||||||
t.Errorf("unexpected mount: %+v", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuilder_WithProc(t *testing.T) {
|
|
||||||
b := NewBuilder().WithProc()
|
|
||||||
if len(b.Mounts) != 1 {
|
|
||||||
t.Fatalf("expected 1 mount, got %d", len(b.Mounts))
|
|
||||||
}
|
|
||||||
m := b.Mounts[0]
|
|
||||||
if m.FsType != "proc" {
|
|
||||||
t.Errorf("expected proc fsType")
|
|
||||||
}
|
|
||||||
if !m.IsReadOnly() {
|
|
||||||
t.Errorf("expected readonly proc mount")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuilder_WithProcRW(t *testing.T) {
|
|
||||||
b := NewBuilder().WithProcRW(true)
|
|
||||||
if len(b.Mounts) != 1 {
|
|
||||||
t.Fatalf("expected 1 mount, got %d", len(b.Mounts))
|
|
||||||
}
|
|
||||||
m := b.Mounts[0]
|
|
||||||
if m.FsType != "proc" {
|
|
||||||
t.Errorf("expected proc fsType")
|
|
||||||
}
|
|
||||||
if m.IsReadOnly() {
|
|
||||||
t.Errorf("expected read-write proc mount")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuilder_WithMounts(t *testing.T) {
|
|
||||||
m1 := Mount{Source: "/a", Target: "/b"}
|
|
||||||
m2 := Mount{Source: "/c", Target: "/d"}
|
|
||||||
b := NewBuilder().WithMounts([]Mount{m1, m2})
|
|
||||||
if len(b.Mounts) != 2 {
|
|
||||||
t.Fatalf("expected 2 mounts, got %d", len(b.Mounts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuilder_WithMount(t *testing.T) {
|
|
||||||
m := Mount{Source: "/a", Target: "/b"}
|
|
||||||
b := NewBuilder().WithMount(m)
|
|
||||||
if len(b.Mounts) != 1 {
|
|
||||||
t.Fatalf("expected 1 mount, got %d", len(b.Mounts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuilder_String(t *testing.T) {
|
|
||||||
b := NewBuilder().
|
|
||||||
WithBind("/src", "/dst", false).
|
|
||||||
WithTmpfs("/tmp", "size=1m").
|
|
||||||
WithProc()
|
|
||||||
s := b.String()
|
|
||||||
if !strings.HasPrefix(s, "Mounts: ") {
|
|
||||||
t.Errorf("unexpected prefix: %q", s)
|
|
||||||
}
|
|
||||||
if !strings.Contains(s, "bind[/src:/dst:rw]") {
|
|
||||||
t.Errorf("missing bind: %q", s)
|
|
||||||
}
|
|
||||||
if !strings.Contains(s, "tmpfs[/tmp]") {
|
|
||||||
t.Errorf("missing tmpfs: %q", s)
|
|
||||||
}
|
|
||||||
if !strings.Contains(s, "proc[ro]") {
|
|
||||||
t.Errorf("missing proc: %q", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuilder_FilterNotExist(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
tmpFilePath := tmpDir + "/mounttest"
|
|
||||||
f, err := os.Create(tmpFilePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
b := NewBuilder().
|
|
||||||
WithBind(f.Name(), "/dst1", false).
|
|
||||||
WithBind("/not/exist", "/dst2", false)
|
|
||||||
b.FilterNotExist()
|
|
||||||
if len(b.Mounts) != 1 {
|
|
||||||
t.Errorf("expected 1 mount after filter, got %d", len(b.Mounts))
|
|
||||||
}
|
|
||||||
if b.Mounts[0].Source != f.Name() {
|
|
||||||
t.Errorf("unexpected mount: %+v", b.Mounts[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,69 +3,21 @@ package mount
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mount calls mount syscall
|
// Mount calls mount syscall
|
||||||
func (m *Mount) Mount() error {
|
func (m *Mount) Mount() error {
|
||||||
if err := ensureMountTargetExists(m.Source, m.Target); err != nil {
|
if err := os.MkdirAll(m.Target, 0755); err != nil {
|
||||||
return fmt.Errorf("mkdir: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
if err := syscall.Mount(m.Source, m.Target, m.FsType, m.Flags, m.Data); err != nil {
|
if err := syscall.Mount(m.Source, m.Target, m.FsType, m.Flags, m.Data); err != nil {
|
||||||
return fmt.Errorf("mount: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
// Read-only bind mount need to be remounted
|
// Read-only bind mount need to be remounted
|
||||||
const bindRo = syscall.MS_BIND | syscall.MS_RDONLY
|
const bindRo = syscall.MS_BIND | syscall.MS_RDONLY
|
||||||
const mask = syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_NOEXEC | syscall.MS_NOATIME | syscall.MS_NODIRATIME | syscall.MS_RELATIME
|
|
||||||
if m.Flags&bindRo == bindRo {
|
if m.Flags&bindRo == bindRo {
|
||||||
// Ensure the flag retains for bind mount
|
if err := syscall.Mount("", m.Target, m.FsType, m.Flags|syscall.MS_REMOUNT, m.Data); err != nil {
|
||||||
var s syscall.Statfs_t
|
|
||||||
if err := syscall.Statfs(m.Source, &s); err != nil {
|
|
||||||
return fmt.Errorf("statfs: %w", err)
|
|
||||||
}
|
|
||||||
flag := m.Flags | syscall.MS_REMOUNT | uintptr(s.Flags&mask)
|
|
||||||
if err := syscall.Mount("", m.Target, m.FsType, flag, m.Data); err != nil {
|
|
||||||
return fmt.Errorf("remount: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsBindMount returns if it is a bind mount
|
|
||||||
func (m Mount) IsBindMount() bool {
|
|
||||||
return m.Flags&syscall.MS_BIND == syscall.MS_BIND
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsReadOnly returns if it is a readonly mount
|
|
||||||
func (m Mount) IsReadOnly() bool {
|
|
||||||
return m.Flags&syscall.MS_RDONLY == syscall.MS_RDONLY
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsTmpFs returns if the fsType is tmpfs
|
|
||||||
func (m Mount) IsTmpFs() bool {
|
|
||||||
return m.FsType == "tmpfs"
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureMountTargetExists(source, target string) error {
|
|
||||||
isFile := false
|
|
||||||
if fi, err := os.Stat(source); err == nil {
|
|
||||||
isFile = !fi.IsDir()
|
|
||||||
}
|
|
||||||
dir := target
|
|
||||||
if isFile {
|
|
||||||
dir = filepath.Dir(target)
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
if err := syscall.Mknod(target, 0755, 0); err != nil {
|
|
||||||
// double check if file exists
|
|
||||||
f, err1 := os.Lstat(target)
|
|
||||||
if err1 == nil && f.Mode().IsRegular() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,19 +25,19 @@ func ensureMountTargetExists(source, target string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Mount) String() string {
|
func (m Mount) String() string {
|
||||||
|
switch {
|
||||||
|
case m.Flags&syscall.MS_BIND == syscall.MS_BIND:
|
||||||
flag := "rw"
|
flag := "rw"
|
||||||
if m.Flags&syscall.MS_RDONLY == syscall.MS_RDONLY {
|
if m.Flags&syscall.MS_RDONLY == syscall.MS_RDONLY {
|
||||||
flag = "ro"
|
flag = "ro"
|
||||||
}
|
}
|
||||||
switch {
|
|
||||||
case m.Flags&syscall.MS_BIND == syscall.MS_BIND:
|
|
||||||
return fmt.Sprintf("bind[%s:%s:%s]", m.Source, m.Target, flag)
|
return fmt.Sprintf("bind[%s:%s:%s]", m.Source, m.Target, flag)
|
||||||
|
|
||||||
case m.FsType == "tmpfs":
|
case m.FsType == "tmpfs":
|
||||||
return fmt.Sprintf("tmpfs[%s]", m.Target)
|
return fmt.Sprintf("tmpfs[%s]", m.Target)
|
||||||
|
|
||||||
case m.FsType == "proc":
|
case m.FsType == "proc":
|
||||||
return fmt.Sprintf("proc[%s]", flag)
|
return fmt.Sprintf("proc[]")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("mount[%s,%s:%s:%x,%s]", m.FsType, m.Source, m.Target, m.Flags, m.Data)
|
return fmt.Sprintf("mount[%s,%s:%s:%x,%s]", m.FsType, m.Source, m.Target, m.Flags, m.Data)
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
package mount
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMount_IsBindMount(t *testing.T) {
|
|
||||||
m := Mount{Flags: syscall.MS_BIND}
|
|
||||||
if !m.IsBindMount() {
|
|
||||||
t.Errorf("expected IsBindMount true")
|
|
||||||
}
|
|
||||||
m.Flags = 0
|
|
||||||
if m.IsBindMount() {
|
|
||||||
t.Errorf("expected IsBindMount false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMount_IsReadOnly(t *testing.T) {
|
|
||||||
m := Mount{Flags: syscall.MS_RDONLY}
|
|
||||||
if !m.IsReadOnly() {
|
|
||||||
t.Errorf("expected IsReadOnly true")
|
|
||||||
}
|
|
||||||
m.Flags = 0
|
|
||||||
if m.IsReadOnly() {
|
|
||||||
t.Errorf("expected IsReadOnly false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMount_IsTmpFs(t *testing.T) {
|
|
||||||
m := Mount{FsType: "tmpfs"}
|
|
||||||
if !m.IsTmpFs() {
|
|
||||||
t.Errorf("expected IsTmpFs true")
|
|
||||||
}
|
|
||||||
m.FsType = "other"
|
|
||||||
if m.IsTmpFs() {
|
|
||||||
t.Errorf("expected IsTmpFs false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMount_String(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
m Mount
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
m: Mount{Source: "/src", Target: "/dst", Flags: syscall.MS_BIND, FsType: "", Data: ""},
|
|
||||||
want: "bind[/src:/dst:rw]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
m: Mount{Source: "/src", Target: "/dst", Flags: syscall.MS_BIND | syscall.MS_RDONLY, FsType: "", Data: ""},
|
|
||||||
want: "bind[/src:/dst:ro]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
m: Mount{Source: "", Target: "/tmp", FsType: "tmpfs"},
|
|
||||||
want: "tmpfs[/tmp]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
m: Mount{Source: "", Target: "proc", FsType: "proc", Flags: syscall.MS_RDONLY},
|
|
||||||
want: "proc[ro]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
m: Mount{Source: "src", Target: "dst", FsType: "other", Flags: 0, Data: "data"},
|
|
||||||
want: "mount[other,src:dst:0,data]",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
got := tt.m.String()
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("Mount.String() = %q, want %q", got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnsureMountTargetExists_Dir(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
target := filepath.Join(tmpDir, "foo/bar")
|
|
||||||
err := ensureMountTargetExists(tmpDir, target)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ensureMountTargetExists error: %v", err)
|
|
||||||
}
|
|
||||||
info, err := os.Stat(target)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("stat error: %v", err)
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
t.Errorf("expected directory at %s", target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnsureMountTargetExists_File(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
srcFile := filepath.Join(tmpDir, "srcfile")
|
|
||||||
if err := os.WriteFile(srcFile, []byte("x"), 0644); err != nil {
|
|
||||||
t.Fatalf("write srcfile: %v", err)
|
|
||||||
}
|
|
||||||
target := filepath.Join(tmpDir, "targetfile")
|
|
||||||
err := ensureMountTargetExists(srcFile, target)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ensureMountTargetExists error: %v", err)
|
|
||||||
}
|
|
||||||
// Should be a file or at least exist
|
|
||||||
info, err := os.Lstat(target)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("lstat error: %v", err)
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
t.Errorf("expected file at %s, got directory", target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,9 +13,9 @@ import (
|
|||||||
// at most max bytes to a buffer
|
// at most max bytes to a buffer
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
W *os.File
|
W *os.File
|
||||||
|
Max int64
|
||||||
Buffer *bytes.Buffer
|
Buffer *bytes.Buffer
|
||||||
Done <-chan struct{}
|
Done <-chan struct{}
|
||||||
Max int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPipe create a pipe with a goroutine to copy its read-end to writer
|
// NewPipe create a pipe with a goroutine to copy its read-end to writer
|
||||||
@ -28,11 +28,9 @@ func NewPipe(writer io.Writer, n int64) (<-chan struct{}, *os.File, error) {
|
|||||||
}
|
}
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
defer r.Close()
|
||||||
io.CopyN(writer, r, int64(n))
|
io.CopyN(writer, r, int64(n))
|
||||||
close(done)
|
|
||||||
// ensure no blocking / SIGPIPE on the other end
|
|
||||||
io.Copy(io.Discard, r)
|
|
||||||
r.Close()
|
|
||||||
}()
|
}()
|
||||||
return done, w, nil
|
return done, w, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,101 +0,0 @@
|
|||||||
package pipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewBuffer_WriteAndRead(t *testing.T) {
|
|
||||||
const max = 10
|
|
||||||
buf, err := NewBuffer(max)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewBuffer error: %v", err)
|
|
||||||
}
|
|
||||||
defer buf.W.Close()
|
|
||||||
|
|
||||||
// Write less than max bytes
|
|
||||||
input := "hello"
|
|
||||||
n, err := buf.W.Write([]byte(input))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Write error: %v", err)
|
|
||||||
}
|
|
||||||
if n != len(input) {
|
|
||||||
t.Errorf("Write bytes = %d, want %d", n, len(input))
|
|
||||||
}
|
|
||||||
buf.W.Close()
|
|
||||||
<-buf.Done
|
|
||||||
|
|
||||||
got := buf.Buffer.String()
|
|
||||||
if got != input {
|
|
||||||
t.Errorf("Buffer content = %q, want %q", got, input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBuffer_MaxBytes(t *testing.T) {
|
|
||||||
const max = 5
|
|
||||||
buf, err := NewBuffer(max)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewBuffer error: %v", err)
|
|
||||||
}
|
|
||||||
defer buf.W.Close()
|
|
||||||
|
|
||||||
// Write more than max bytes
|
|
||||||
input := "toolonginput"
|
|
||||||
_, err = io.Copy(buf.W, strings.NewReader(input))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Copy error: %v", err)
|
|
||||||
}
|
|
||||||
buf.W.Close()
|
|
||||||
<-buf.Done
|
|
||||||
|
|
||||||
got := buf.Buffer.String()
|
|
||||||
if len(got) != int(max+1) {
|
|
||||||
t.Errorf("Buffer length = %d, want %d", len(got), max+1)
|
|
||||||
}
|
|
||||||
if got != input[:max+1] {
|
|
||||||
t.Errorf("Buffer content = %q, want %q", got, input[:max+1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuffer_String(t *testing.T) {
|
|
||||||
const max = 8
|
|
||||||
buf, err := NewBuffer(max)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewBuffer error: %v", err)
|
|
||||||
}
|
|
||||||
defer buf.W.Close()
|
|
||||||
|
|
||||||
_, _ = buf.W.Write([]byte("abc"))
|
|
||||||
buf.W.Close()
|
|
||||||
<-buf.Done
|
|
||||||
|
|
||||||
want := "Buffer[3/8]"
|
|
||||||
if buf.String() != want {
|
|
||||||
t.Errorf("String() = %q, want %q", buf.String(), want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBuffer_DoneCloses(t *testing.T) {
|
|
||||||
const max = 4
|
|
||||||
buf, err := NewBuffer(max)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewBuffer error: %v", err)
|
|
||||||
}
|
|
||||||
defer buf.W.Close()
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
_, _ = buf.W.Write([]byte("test"))
|
|
||||||
buf.W.Close()
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-buf.Done:
|
|
||||||
// ok
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
t.Fatal("timeout waiting for Done channel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -17,8 +17,6 @@ type RLimits struct {
|
|||||||
FileSize uint64 // in bytes
|
FileSize uint64 // in bytes
|
||||||
Stack uint64 // in bytes
|
Stack uint64 // in bytes
|
||||||
AddressSpace uint64 // in bytes
|
AddressSpace uint64 // in bytes
|
||||||
OpenFile uint64 // count
|
|
||||||
DisableCore bool // set core to 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RLimit is the resource limits defined by Linux setrlimit
|
// RLimit is the resource limits defined by Linux setrlimit
|
||||||
@ -72,28 +70,15 @@ func (r *RLimits) PrepareRLimit() []RLimit {
|
|||||||
Rlim: getRlimit(r.AddressSpace, r.AddressSpace),
|
Rlim: getRlimit(r.AddressSpace, r.AddressSpace),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if r.OpenFile > 0 {
|
|
||||||
ret = append(ret, RLimit{
|
|
||||||
Res: syscall.RLIMIT_NOFILE,
|
|
||||||
Rlim: getRlimit(r.OpenFile, r.OpenFile),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if r.DisableCore {
|
|
||||||
ret = append(ret, RLimit{
|
|
||||||
Res: syscall.RLIMIT_CORE,
|
|
||||||
Rlim: getRlimit(0, 0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r RLimit) String() string {
|
func (r RLimit) String() string {
|
||||||
|
if r.Res == syscall.RLIMIT_CPU {
|
||||||
|
return fmt.Sprintf("CPU[%d s:%d s]", r.Rlim.Cur, r.Rlim.Max)
|
||||||
|
}
|
||||||
t := ""
|
t := ""
|
||||||
switch r.Res {
|
switch r.Res {
|
||||||
case syscall.RLIMIT_CPU:
|
|
||||||
return fmt.Sprintf("CPU[%d s:%d s]", r.Rlim.Cur, r.Rlim.Max)
|
|
||||||
case syscall.RLIMIT_NOFILE:
|
|
||||||
return fmt.Sprintf("OpenFile[%d:%d]", r.Rlim.Cur, r.Rlim.Max)
|
|
||||||
case syscall.RLIMIT_DATA:
|
case syscall.RLIMIT_DATA:
|
||||||
t = "Data"
|
t = "Data"
|
||||||
case syscall.RLIMIT_FSIZE:
|
case syscall.RLIMIT_FSIZE:
|
||||||
@ -102,8 +87,6 @@ func (r RLimit) String() string {
|
|||||||
t = "Stack"
|
t = "Stack"
|
||||||
case syscall.RLIMIT_AS:
|
case syscall.RLIMIT_AS:
|
||||||
t = "AddressSpace"
|
t = "AddressSpace"
|
||||||
case syscall.RLIMIT_CORE:
|
|
||||||
t = "Core"
|
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s[%v:%v]", t, runner.Size(r.Rlim.Cur), runner.Size(r.Rlim.Max))
|
return fmt.Sprintf("%s[%v:%v]", t, runner.Size(r.Rlim.Cur), runner.Size(r.Rlim.Max))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,136 +0,0 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package rlimit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPrepareRLimit(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
rl RLimits
|
|
||||||
expect []int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Empty",
|
|
||||||
rl: RLimits{},
|
|
||||||
expect: []int{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CPU only",
|
|
||||||
rl: RLimits{CPU: 1},
|
|
||||||
expect: []int{syscall.RLIMIT_CPU},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Data only",
|
|
||||||
rl: RLimits{Data: 1024},
|
|
||||||
expect: []int{syscall.RLIMIT_DATA},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "All fields",
|
|
||||||
rl: RLimits{CPU: 1, CPUHard: 2, Data: 1024, FileSize: 2048, Stack: 4096, AddressSpace: 8192, OpenFile: 16, DisableCore: true},
|
|
||||||
expect: []int{syscall.RLIMIT_CPU, syscall.RLIMIT_DATA, syscall.RLIMIT_FSIZE, syscall.RLIMIT_STACK, syscall.RLIMIT_AS, syscall.RLIMIT_NOFILE, syscall.RLIMIT_CORE},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "DisableCore only",
|
|
||||||
rl: RLimits{DisableCore: true},
|
|
||||||
expect: []int{syscall.RLIMIT_CORE},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
rls := tt.rl.PrepareRLimit()
|
|
||||||
if len(rls) != len(tt.expect) {
|
|
||||||
t.Fatalf("expected %d rlimits, got %d", len(tt.expect), len(rls))
|
|
||||||
}
|
|
||||||
for i, r := range rls {
|
|
||||||
if r.Res != tt.expect[i] {
|
|
||||||
t.Errorf("expected Res %d at %d, got %d", tt.expect[i], i, r.Res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRLimitString(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
rl RLimit
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "CPU",
|
|
||||||
rl: RLimit{Res: syscall.RLIMIT_CPU, Rlim: syscall.Rlimit{Cur: 1, Max: 2}},
|
|
||||||
want: "CPU[1 s:2 s]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "NOFILE",
|
|
||||||
rl: RLimit{Res: syscall.RLIMIT_NOFILE, Rlim: syscall.Rlimit{Cur: 10, Max: 20}},
|
|
||||||
want: "OpenFile[10:20]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "DATA",
|
|
||||||
rl: RLimit{Res: syscall.RLIMIT_DATA, Rlim: syscall.Rlimit{Cur: 1024, Max: 2048}},
|
|
||||||
want: "Data[1.0 KiB:2.0 KiB]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "FSIZE",
|
|
||||||
rl: RLimit{Res: syscall.RLIMIT_FSIZE, Rlim: syscall.Rlimit{Cur: 100, Max: 200}},
|
|
||||||
want: "File[100 B:200 B]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "STACK",
|
|
||||||
rl: RLimit{Res: syscall.RLIMIT_STACK, Rlim: syscall.Rlimit{Cur: 4096, Max: 8192}},
|
|
||||||
want: "Stack[4.0 KiB:8.0 KiB]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "AS",
|
|
||||||
rl: RLimit{Res: syscall.RLIMIT_AS, Rlim: syscall.Rlimit{Cur: 123, Max: 456}},
|
|
||||||
want: "AddressSpace[123 B:456 B]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CORE",
|
|
||||||
rl: RLimit{Res: syscall.RLIMIT_CORE, Rlim: syscall.Rlimit{Cur: 0, Max: 0}},
|
|
||||||
want: "Core[0 B:0 B]",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := tt.rl.String()
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("got %q, want %q", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRLimitsString(t *testing.T) {
|
|
||||||
rl := RLimits{
|
|
||||||
CPU: 1,
|
|
||||||
CPUHard: 2,
|
|
||||||
Data: 1024,
|
|
||||||
FileSize: 2048,
|
|
||||||
Stack: 4096,
|
|
||||||
AddressSpace: 8192,
|
|
||||||
OpenFile: 16,
|
|
||||||
DisableCore: true,
|
|
||||||
}
|
|
||||||
want := "RLimits[CPU[1 s:2 s],Data[1.0 KiB:1.0 KiB],File[2.0 KiB:2.0 KiB],Stack[4.0 KiB:4.0 KiB],AddressSpace[8.0 KiB:8.0 KiB],OpenFile[16:16],Core[0 B:0 B]]"
|
|
||||||
got := rl.String()
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("got %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRLimitsString_Empty(t *testing.T) {
|
|
||||||
rl := RLimits{}
|
|
||||||
want := "RLimits[]"
|
|
||||||
got := rl.String()
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("got %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package libseccomp
|
package seccomp
|
||||||
|
|
||||||
// Action is seccomp trap action
|
// Action is seccomp trap action
|
||||||
type Action uint32
|
type Action uint32
|
||||||
@ -19,6 +19,16 @@ const (
|
|||||||
MsgHandle
|
MsgHandle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WithReturnCode set the return code when action is trace or ban
|
||||||
|
func (a Action) WithReturnCode(code int16) Action {
|
||||||
|
return a.Action() | Action(code)<<16
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnCode get the return code
|
||||||
|
func (a Action) ReturnCode() int16 {
|
||||||
|
return int16(a >> 16)
|
||||||
|
}
|
||||||
|
|
||||||
// Action get the basic action
|
// Action get the basic action
|
||||||
func (a Action) Action() Action {
|
func (a Action) Action() Action {
|
||||||
return Action(a & 0xffff)
|
return Action(a & 0xffff)
|
||||||
5
pkg/seccomp/filter.go
Normal file
5
pkg/seccomp/filter.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// Package seccomp provides a generated filter format for seccomp filter
|
||||||
|
package seccomp
|
||||||
|
|
||||||
|
// Filter is the BPF seccomp filter value
|
||||||
|
type Filter []byte
|
||||||
@ -1,16 +1,15 @@
|
|||||||
// Package seccomp provides a generated filter format for seccomp filter
|
|
||||||
package seccomp
|
package seccomp
|
||||||
|
|
||||||
import "syscall"
|
import (
|
||||||
|
"syscall"
|
||||||
// Filter is the BPF seccomp filter value
|
"unsafe"
|
||||||
type Filter []syscall.SockFilter
|
)
|
||||||
|
|
||||||
// SockFprog converts Filter to SockFprog for seccomp syscall
|
// SockFprog converts Filter to SockFprog for seccomp syscall
|
||||||
func (f Filter) SockFprog() *syscall.SockFprog {
|
func (f Filter) SockFprog() *syscall.SockFprog {
|
||||||
b := []syscall.SockFilter(f)
|
b := []byte(f)
|
||||||
return &syscall.SockFprog{
|
return &syscall.SockFprog{
|
||||||
Len: uint16(len(b)),
|
Len: uint16(len(b) / 8),
|
||||||
Filter: &b[0],
|
Filter: (*syscall.SockFilter)(unsafe.Pointer(&b[0])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,23 @@
|
|||||||
package libseccomp
|
package libseccomp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
libseccomp "github.com/elastic/go-seccomp-bpf"
|
"github.com/criyle/go-sandbox/pkg/seccomp"
|
||||||
|
libseccomp "github.com/seccomp/libseccomp-golang"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToSeccompAction convert action to libseccomp compatible action
|
// ToSeccompAction convert action to libseccomp compatible action
|
||||||
func ToSeccompAction(a Action) libseccomp.Action {
|
func ToSeccompAction(a seccomp.Action) libseccomp.ScmpAction {
|
||||||
var action libseccomp.Action
|
var action libseccomp.ScmpAction
|
||||||
switch a.Action() {
|
switch a.Action() {
|
||||||
case ActionAllow:
|
case seccomp.ActionAllow:
|
||||||
action = libseccomp.ActionAllow
|
action = libseccomp.ActAllow
|
||||||
case ActionErrno:
|
case seccomp.ActionErrno:
|
||||||
action = libseccomp.ActionErrno
|
action = libseccomp.ActErrno
|
||||||
case ActionTrace:
|
case seccomp.ActionTrace:
|
||||||
action = libseccomp.ActionTrace
|
action = libseccomp.ActTrace
|
||||||
default:
|
default:
|
||||||
action = libseccomp.ActionKillProcess
|
action = libseccomp.ActKill
|
||||||
}
|
}
|
||||||
// the least 16 bit of ret value is SECCOMP_RET_DATA
|
action = action.SetReturnCode(a.ReturnCode())
|
||||||
// although it might not officially supported by go-seccomp-bpf
|
|
||||||
// action = action.WithReturnData(int(a.ReturnCode()))
|
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,61 +1,75 @@
|
|||||||
package libseccomp
|
package libseccomp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"syscall"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/seccomp"
|
"github.com/criyle/go-sandbox/pkg/seccomp"
|
||||||
libseccomp "github.com/elastic/go-seccomp-bpf"
|
libseccomp "github.com/seccomp/libseccomp-golang"
|
||||||
"golang.org/x/net/bpf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Builder is used to build the filter
|
// Builder is used to build the filter
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
Allow, Trace []string
|
Allow, Trace []string
|
||||||
Default Action
|
Default seccomp.Action
|
||||||
}
|
}
|
||||||
|
|
||||||
var actTrace = libseccomp.ActionTrace
|
var actTrace = libseccomp.ActTrace.SetReturnCode(seccomp.MsgHandle)
|
||||||
|
|
||||||
// Build builds the filter
|
// Build builds the filter
|
||||||
func (b *Builder) Build() (seccomp.Filter, error) {
|
func (b *Builder) Build() (seccomp.Filter, error) {
|
||||||
policy := libseccomp.Policy{
|
filter, err := libseccomp.NewFilter(ToSeccompAction(b.Default))
|
||||||
DefaultAction: ToSeccompAction(b.Default),
|
|
||||||
Syscalls: []libseccomp.SyscallGroup{
|
|
||||||
{
|
|
||||||
Action: libseccomp.ActionAllow,
|
|
||||||
Names: b.Allow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: actTrace,
|
|
||||||
Names: b.Trace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
program, err := policy.Assemble()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ExportBPF(program)
|
defer filter.Release()
|
||||||
|
|
||||||
|
if err = addFilterActions(filter, b.Allow, libseccomp.ActAllow); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = addFilterActions(filter, b.Trace, actTrace); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ExportBPF(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportBPF convert libseccomp filter to kernel readable BPF content
|
// ExportBPF convert libseccomp filter to kernel readable BPF content
|
||||||
func ExportBPF(filter []bpf.Instruction) (seccomp.Filter, error) {
|
func ExportBPF(filter *libseccomp.ScmpFilter) (seccomp.Filter, error) {
|
||||||
raw, err := bpf.Assemble(filter)
|
r, w, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return sockFilter(raw), nil
|
defer r.Close()
|
||||||
|
// export BPF to pipe
|
||||||
|
go func() {
|
||||||
|
filter.ExportBPF(w)
|
||||||
|
w.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// get BPF binary
|
||||||
|
bin, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return seccomp.Filter(bin), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sockFilter(raw []bpf.RawInstruction) []syscall.SockFilter {
|
func addFilterActions(filter *libseccomp.ScmpFilter, names []string, action libseccomp.ScmpAction) error {
|
||||||
filter := make([]syscall.SockFilter, 0, len(raw))
|
for _, s := range names {
|
||||||
for _, instruction := range raw {
|
if err := addFilterAction(filter, s, action); err != nil {
|
||||||
filter = append(filter, syscall.SockFilter{
|
return err
|
||||||
Code: instruction.Op,
|
|
||||||
Jt: instruction.Jt,
|
|
||||||
Jf: instruction.Jf,
|
|
||||||
K: instruction.K,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return filter
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFilterAction(filter *libseccomp.ScmpFilter, name string, action libseccomp.ScmpAction) error {
|
||||||
|
syscallID, err := libseccomp.GetSyscallFromName(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = filter.AddRule(syscallID, action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/criyle/go-sandbox/pkg/seccomp"
|
"github.com/criyle/go-sandbox/pkg/seccomp"
|
||||||
|
libseccomp "github.com/seccomp/libseccomp-golang"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -21,7 +22,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildFilter(t *testing.T) {
|
func TestBuildFilter(t *testing.T) {
|
||||||
_, err := buildFilterMock()
|
defaultAction := libseccomp.ActKill
|
||||||
|
_, err := buildFilterMock(defaultAction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("BuildFilter failed")
|
t.Error("BuildFilter failed")
|
||||||
}
|
}
|
||||||
@ -33,17 +35,17 @@ func BenchmarkBuildDefaultFilter(b *testing.B) {
|
|||||||
builder := Builder{
|
builder := Builder{
|
||||||
Allow: defaultSyscallAllows,
|
Allow: defaultSyscallAllows,
|
||||||
Trace: defaultSyscallTraces,
|
Trace: defaultSyscallTraces,
|
||||||
Default: ActionTrace,
|
Default: seccomp.ActionTrace,
|
||||||
}
|
}
|
||||||
builder.Build()
|
builder.Build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFilterMock() (seccomp.Filter, error) {
|
func buildFilterMock(d libseccomp.ScmpAction) (seccomp.Filter, error) {
|
||||||
b := Builder{
|
b := Builder{
|
||||||
Allow: []string{"fork"},
|
Allow: []string{"fork"},
|
||||||
Trace: []string{"execve"},
|
Trace: []string{"execve"},
|
||||||
Default: ActionTrace,
|
Default: seccomp.ActionTrace,
|
||||||
}
|
}
|
||||||
return b.Build()
|
return b.Build()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,10 @@
|
|||||||
package libseccomp
|
package libseccomp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
libseccomp "github.com/seccomp/libseccomp-golang"
|
||||||
|
|
||||||
"github.com/elastic/go-seccomp-bpf/arch"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var info, errInfo = arch.GetInfo("")
|
|
||||||
|
|
||||||
// ToSyscallName convert syscallno to syscall name
|
// ToSyscallName convert syscallno to syscall name
|
||||||
func ToSyscallName(sysno uint) (string, error) {
|
func ToSyscallName(sysno uint) (string, error) {
|
||||||
if errInfo != nil {
|
return libseccomp.ScmpSyscall(sysno).GetName()
|
||||||
return "", errInfo
|
|
||||||
}
|
|
||||||
n, ok := info.SyscallNumbers[int(sysno)]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("syscall number does not exist: %d", sysno)
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
package unixsocket
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func BenchmarkBaseline(b *testing.B) {
|
|
||||||
s, t, err := NewSocketPair()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
m := make([]byte, 1024)
|
|
||||||
b.ResetTimer()
|
|
||||||
go func() {
|
|
||||||
msg := []byte("message")
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
s.SendMsg(msg, Msg{})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
t.RecvMsg(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkGoroutine(b *testing.B) {
|
|
||||||
s, t, err := NewSocketPair()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
m := make([]byte, 1024)
|
|
||||||
b.ResetTimer()
|
|
||||||
go func() {
|
|
||||||
msg := []byte("message")
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
s.SendMsg(msg, Msg{})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
c := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(c)
|
|
||||||
t.RecvMsg(m)
|
|
||||||
}()
|
|
||||||
<-c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkChannel(b *testing.B) {
|
|
||||||
c := make(chan []byte)
|
|
||||||
benchGoroutine(b, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkChannelBuffed(b *testing.B) {
|
|
||||||
c := make(chan []byte, 1)
|
|
||||||
benchGoroutine(b, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkChannelBuffed4(b *testing.B) {
|
|
||||||
c := make(chan []byte, 4)
|
|
||||||
benchGoroutine(b, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkEmptyGoroutine(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
c := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
close(c)
|
|
||||||
}()
|
|
||||||
<-c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func benchGoroutine(b *testing.B, c chan []byte) {
|
|
||||||
s, t, err := NewSocketPair()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
msg := []byte("message")
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
s.SendMsg(msg, Msg{})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
go func() {
|
|
||||||
m := make([]byte, 1024)
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
t.RecvMsg(m)
|
|
||||||
c <- m
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
<-c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,18 +7,22 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// oob size default to page size
|
// oob size default to page size
|
||||||
const oobSize = 4 << 10 // 4kb
|
const oobSize = 4096
|
||||||
|
|
||||||
|
// use pool to minimize allocate gabage collector overhead
|
||||||
|
var oobPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return make([]byte, oobSize)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Socket wrappers a unix socket connection
|
// Socket wrappers a unix socket connection
|
||||||
type Socket struct {
|
type Socket net.UnixConn
|
||||||
*net.UnixConn
|
|
||||||
sendBuff []byte
|
|
||||||
recvBuff []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Msg is the oob msg with the message
|
// Msg is the oob msg with the message
|
||||||
type Msg struct {
|
type Msg struct {
|
||||||
@ -26,61 +30,51 @@ type Msg struct {
|
|||||||
Cred *syscall.Ucred // unix credential
|
Cred *syscall.Ucred // unix credential
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSocket(conn *net.UnixConn) *Socket {
|
|
||||||
return &Socket{
|
|
||||||
UnixConn: conn,
|
|
||||||
sendBuff: make([]byte, oobSize),
|
|
||||||
recvBuff: make([]byte, oobSize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSocket creates Socket conn struct using existing unix socket fd
|
// NewSocket creates Socket conn struct using existing unix socket fd
|
||||||
// creates by socketpair or net.DialUnix and mark it as close_on_exec (avoid fd leak)
|
// creates by socketpair or net.DialUnix and mark it as close_on_exec (avoid fd leak)
|
||||||
// it need SOCK_SEQPACKET socket for reliable transfer
|
// it need SOCK_SEQPACKET socket for reliable transfer
|
||||||
// it will need SO_PASSCRED to pass unix credential, Notice: in the documentation,
|
// it will need SO_PASSCRED to pass unix credential, Notice: in the documentation,
|
||||||
// if cred is not specified, self information will be sent
|
// if cred is not specified, self information will be sent
|
||||||
func NewSocket(fd int) (*Socket, error) {
|
func NewSocket(fd int) (*Socket, error) {
|
||||||
syscall.SetNonblock(fd, true)
|
|
||||||
syscall.CloseOnExec(fd)
|
|
||||||
|
|
||||||
file := os.NewFile(uintptr(fd), "unix-socket")
|
file := os.NewFile(uintptr(fd), "unix-socket")
|
||||||
if file == nil {
|
if file == nil {
|
||||||
return nil, fmt.Errorf("new socket: %d is not a valid fd", fd)
|
return nil, fmt.Errorf("NewSocket: %d is not a valid fd", fd)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
syscall.CloseOnExec(int(file.Fd()))
|
||||||
|
|
||||||
conn, err := net.FileConn(file)
|
conn, err := net.FileConn(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("new socket: fileconn: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
unixConn, ok := conn.(*net.UnixConn)
|
unixConn, ok := conn.(*net.UnixConn)
|
||||||
if !ok {
|
if !ok {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, fmt.Errorf("new socket: %d is not a valid unix socket connection", fd)
|
return nil, fmt.Errorf("NewSocket: %d is not a valid unix socket connection", fd)
|
||||||
}
|
}
|
||||||
return newSocket(unixConn), nil
|
return (*Socket)(unixConn), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSocketPair creates connected unix socketpair using SOCK_SEQPACKET
|
// NewSocketPair creates connected unix socketpair using SOCK_SEQPACKET
|
||||||
func NewSocketPair() (*Socket, *Socket, error) {
|
func NewSocketPair() (*Socket, *Socket, error) {
|
||||||
fd, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_SEQPACKET|syscall.SOCK_CLOEXEC, 0)
|
fd, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_SEQPACKET|syscall.SOCK_CLOEXEC, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("new socket pair: socketpair: %w", err)
|
return nil, nil, fmt.Errorf("NewSocketPair: failed to call socketpair %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ins, err := NewSocket(fd[0])
|
ins, err := NewSocket(fd[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
syscall.Close(fd[0])
|
syscall.Close(fd[0])
|
||||||
syscall.Close(fd[1])
|
syscall.Close(fd[1])
|
||||||
return nil, nil, fmt.Errorf("new socket pair: sender: %w", err)
|
return nil, nil, fmt.Errorf("NewSocketPair: failed to call NewSocket on sender %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
outs, err := NewSocket(fd[1])
|
outs, err := NewSocket(fd[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ins.Close()
|
ins.Close()
|
||||||
syscall.Close(fd[1])
|
syscall.Close(fd[1])
|
||||||
return nil, nil, fmt.Errorf("new socket pair: receiver: %w", err)
|
return nil, nil, fmt.Errorf("NewSocketPair: failed to call NewSocket receiver %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ins, outs, nil
|
return ins, outs, nil
|
||||||
@ -88,7 +82,7 @@ func NewSocketPair() (*Socket, *Socket, error) {
|
|||||||
|
|
||||||
// SetPassCred set sockopt for pass cred for unix socket
|
// SetPassCred set sockopt for pass cred for unix socket
|
||||||
func (s *Socket) SetPassCred(option int) error {
|
func (s *Socket) SetPassCred(option int) error {
|
||||||
sysconn, err := s.SyscallConn()
|
sysconn, err := (*net.UnixConn)(s).SyscallConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -98,16 +92,21 @@ func (s *Socket) SetPassCred(option int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendMsg sendmsg to unix socket and encode possible unix right / credential
|
// SendMsg sendmsg to unix socket and encode possible unix right / credential
|
||||||
func (s *Socket) SendMsg(b []byte, m Msg) error {
|
func (s *Socket) SendMsg(b []byte, m *Msg) error {
|
||||||
oob := bytes.NewBuffer(s.sendBuff[:0])
|
buf := oobPool.Get().([]byte)
|
||||||
|
defer oobPool.Put(buf)
|
||||||
|
|
||||||
|
oob := bytes.NewBuffer(buf[:0])
|
||||||
|
if m != nil {
|
||||||
if len(m.Fds) > 0 {
|
if len(m.Fds) > 0 {
|
||||||
oob.Write(syscall.UnixRights(m.Fds...))
|
oob.Write(syscall.UnixRights(m.Fds...))
|
||||||
}
|
}
|
||||||
if m.Cred != nil {
|
if m.Cred != nil {
|
||||||
oob.Write(syscall.UnixCredentials(m.Cred))
|
oob.Write(syscall.UnixCredentials(m.Cred))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, _, err := s.WriteMsgUnix(b, oob.Bytes(), nil)
|
_, _, err := (*net.UnixConn)(s).WriteMsgUnix(b, oob.Bytes(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -115,33 +114,28 @@ func (s *Socket) SendMsg(b []byte, m Msg) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RecvMsg recvmsg from unix socket and parse possible unix right / credential
|
// RecvMsg recvmsg from unix socket and parse possible unix right / credential
|
||||||
func (s *Socket) RecvMsg(b []byte) (int, Msg, error) {
|
func (s *Socket) RecvMsg(b []byte) (int, *Msg, error) {
|
||||||
var msg Msg
|
oob := oobPool.Get().([]byte)
|
||||||
n, oobn, _, _, err := s.ReadMsgUnix(b, s.recvBuff)
|
defer oobPool.Put(oob)
|
||||||
|
|
||||||
|
n, oobn, _, _, err := (*net.UnixConn)(s).ReadMsgUnix(b, oob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, msg, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
// parse oob msg
|
// parse oob msg
|
||||||
msgs, err := syscall.ParseSocketControlMessage(s.recvBuff[:oobn])
|
msgs, err := syscall.ParseSocketControlMessage(oob[:oobn])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, msg, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
msg, err = parseMsg(msgs)
|
msg, err := parseMsg(msgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, msg, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
return n, msg, nil
|
return n, msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMsg(msgs []syscall.SocketControlMessage) (msg Msg, err error) {
|
func parseMsg(msgs []syscall.SocketControlMessage) (*Msg, error) {
|
||||||
defer func() {
|
var msg Msg
|
||||||
if err != nil {
|
|
||||||
for _, f := range msg.Fds {
|
|
||||||
syscall.Close(f)
|
|
||||||
}
|
|
||||||
msg.Fds = nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for _, m := range msgs {
|
for _, m := range msgs {
|
||||||
if m.Header.Level != syscall.SOL_SOCKET {
|
if m.Header.Level != syscall.SOL_SOCKET {
|
||||||
continue
|
continue
|
||||||
@ -151,17 +145,17 @@ func parseMsg(msgs []syscall.SocketControlMessage) (msg Msg, err error) {
|
|||||||
case syscall.SCM_CREDENTIALS:
|
case syscall.SCM_CREDENTIALS:
|
||||||
cred, err := syscall.ParseUnixCredentials(&m)
|
cred, err := syscall.ParseUnixCredentials(&m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return msg, err
|
return nil, err
|
||||||
}
|
}
|
||||||
msg.Cred = cred
|
msg.Cred = cred
|
||||||
|
|
||||||
case syscall.SCM_RIGHTS:
|
case syscall.SCM_RIGHTS:
|
||||||
fds, err := syscall.ParseUnixRights(&m)
|
fds, err := syscall.ParseUnixRights(&m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return msg, err
|
return nil, err
|
||||||
}
|
}
|
||||||
msg.Fds = fds
|
msg.Fds = fds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return msg, nil
|
return &msg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,142 +0,0 @@
|
|||||||
package unixsocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBaseline(t *testing.T) {
|
|
||||||
a, b, err := NewSocketPair()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
m := make([]byte, 1024)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
msg := []byte("message")
|
|
||||||
a.SendMsg(msg, Msg{})
|
|
||||||
}()
|
|
||||||
|
|
||||||
n, _, err := b.RecvMsg(m)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(m[:n], []byte("message")) {
|
|
||||||
t.Fatal("not equal")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendRecvMsg_Fds(t *testing.T) {
|
|
||||||
a, b, err := NewSocketPair()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer a.Close()
|
|
||||||
defer b.Close()
|
|
||||||
|
|
||||||
// Create a file to send its fd
|
|
||||||
tmpfile, err := os.CreateTemp("", "unixsocket-fd")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpfile.Name())
|
|
||||||
defer tmpfile.Close()
|
|
||||||
|
|
||||||
msg := []byte("fdtest")
|
|
||||||
go func() {
|
|
||||||
a.SendMsg(msg, Msg{Fds: []int{int(tmpfile.Fd())}})
|
|
||||||
}()
|
|
||||||
|
|
||||||
buf := make([]byte, 64)
|
|
||||||
n, m, err := b.RecvMsg(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(buf[:n], msg) {
|
|
||||||
t.Errorf("RecvMsg got %q, want %q", buf[:n], msg)
|
|
||||||
}
|
|
||||||
if len(m.Fds) != 1 {
|
|
||||||
t.Errorf("expected 1 fd, got %d", len(m.Fds))
|
|
||||||
}
|
|
||||||
if m.Fds != nil {
|
|
||||||
syscall.Close(m.Fds[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendRecvMsg_Cred(t *testing.T) {
|
|
||||||
if os.Geteuid() != 0 {
|
|
||||||
t.Skip("skipping credential test: requires root privileges")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
a, b, err := NewSocketPair()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer a.Close()
|
|
||||||
defer b.Close()
|
|
||||||
|
|
||||||
// Enable credential passing
|
|
||||||
if err := a.SetPassCred(1); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := b.SetPassCred(1); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := []byte("credtest")
|
|
||||||
go func() {
|
|
||||||
a.SendMsg(msg, Msg{Cred: &syscall.Ucred{Pid: 123, Uid: 456, Gid: 789}})
|
|
||||||
}()
|
|
||||||
|
|
||||||
buf := make([]byte, 64)
|
|
||||||
n, m, err := b.RecvMsg(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(buf[:n], msg) {
|
|
||||||
t.Errorf("RecvMsg got %q, want %q", buf[:n], msg)
|
|
||||||
}
|
|
||||||
if m.Cred == nil {
|
|
||||||
t.Error("expected credential, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSocketPair_Close(t *testing.T) {
|
|
||||||
a, b, err := NewSocketPair()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := a.Close(); err != nil {
|
|
||||||
t.Errorf("a.Close() error: %v", err)
|
|
||||||
}
|
|
||||||
if err := b.Close(); err != nil {
|
|
||||||
t.Errorf("b.Close() error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSocket_InvalidFd(t *testing.T) {
|
|
||||||
// Use an invalid fd
|
|
||||||
_, err := NewSocket(-1)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for invalid fd, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetPassCred_InvalidSocket(t *testing.T) {
|
|
||||||
a, b, err := NewSocketPair()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer a.Close()
|
|
||||||
defer b.Close()
|
|
||||||
|
|
||||||
// Close the socket to make it invalid
|
|
||||||
a.Close()
|
|
||||||
err = a.SetPassCred(1)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error on SetPassCred for closed socket, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -36,37 +36,30 @@ func getIovecs(base *byte, l int) []unix.Iovec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func vmReadStr(pid int, addr uintptr, buff []byte) error {
|
func vmReadStr(pid int, addr uintptr, buff []byte) error {
|
||||||
// Handle unaligned address: calculate remaining bytes to page boundary
|
// Deal with unaligned addr
|
||||||
totalRead := 0 // Total bytes read so far
|
n := 0
|
||||||
// Calculate distance to next page boundary, nextRead is the number of bytes to read
|
r := pageSize - int(addr%uintptr(pageSize))
|
||||||
nextRead := pageSize - int(addr%uintptr(pageSize))
|
if r == 0 {
|
||||||
if nextRead == 0 {
|
r = pageSize
|
||||||
nextRead = pageSize // If exactly at page boundary, use full page size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read in a loop until buffer is full or termination condition is met
|
|
||||||
for len(buff) > 0 {
|
for len(buff) > 0 {
|
||||||
// If remaining buffer is smaller than planned read size, reduce read size
|
if l := len(buff); r < l {
|
||||||
if restToRead := len(buff); restToRead < nextRead {
|
r = l
|
||||||
nextRead = restToRead
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read data from current position
|
nn, err := vmRead(pid, addr+uintptr(n), buff[:r])
|
||||||
curRead, err := vmRead(pid, addr+uintptr(totalRead), buff[:nextRead])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err // Read error
|
return err
|
||||||
}
|
|
||||||
if curRead == 0 {
|
|
||||||
break // No more data to read
|
|
||||||
}
|
|
||||||
if hasNull(buff[:curRead]) {
|
|
||||||
break // Found string terminator
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update counters and buffer
|
if hasNull(buff[:nn]) {
|
||||||
totalRead += curRead // Update total bytes read
|
return nil
|
||||||
buff = buff[curRead:] // Move buffer pointer
|
}
|
||||||
nextRead = pageSize // Reset to full page size
|
|
||||||
|
n += nn
|
||||||
|
buff = buff[nn:]
|
||||||
|
r = pageSize
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,250 +0,0 @@
|
|||||||
package ptracer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestHasNull tests the hasNull function
|
|
||||||
func TestHasNull(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data []byte
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty buffer",
|
|
||||||
data: []byte{},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no null",
|
|
||||||
data: []byte("hello"),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "has null at start",
|
|
||||||
data: []byte{0, 1, 2, 3},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "has null at end",
|
|
||||||
data: []byte{1, 2, 3, 0},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "has null in middle",
|
|
||||||
data: []byte{1, 0, 3, 4},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := hasNull(tt.data); got != tt.want {
|
|
||||||
t.Errorf("hasNull() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function: creates a child process and returns its PID
|
|
||||||
func createTestProcess(t *testing.T) (int, func()) {
|
|
||||||
cmd := exec.Command("sleep", "10") // use sleep command to create a running process
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
t.Fatalf("Failed to start test process: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup := func() {
|
|
||||||
cmd.Process.Kill()
|
|
||||||
cmd.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd.Process.Pid, cleanup
|
|
||||||
}
|
|
||||||
|
|
||||||
// findReadableMemoryRegion finds a readable memory region in the process's address space
|
|
||||||
func findReadableMemoryRegion(t *testing.T, pid int, minSize int) uintptr {
|
|
||||||
maps, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read process maps: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range bytes.Split(maps, []byte{'\n'}) {
|
|
||||||
if len(line) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if bytes.Contains(line, []byte("r-x")) {
|
|
||||||
// Parse address range: start-end
|
|
||||||
var start, end uint64
|
|
||||||
_, err := fmt.Sscanf(string(line), "%x-%x", &start, &end)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate region size
|
|
||||||
size := end - start
|
|
||||||
if size >= uint64(minSize) {
|
|
||||||
return uintptr(start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatalf("Failed to find readable memory region with minimum size %d", minSize)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVmRead tests the vmRead function
|
|
||||||
func TestVmRead(t *testing.T) {
|
|
||||||
pid, cleanup := createTestProcess(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
buff := make([]byte, 100)
|
|
||||||
// Ensure the found memory region is large enough
|
|
||||||
baseAddr := findReadableMemoryRegion(t, pid, len(buff))
|
|
||||||
|
|
||||||
n, err := vmRead(pid, baseAddr, buff)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("vmRead() error = %v", err)
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
t.Error("vmRead returned 0 bytes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// vmReadStrTestCase defines a test case for vmReadStr
|
|
||||||
type vmReadStrTestCase struct {
|
|
||||||
name string
|
|
||||||
buffSize int
|
|
||||||
addrAlign uintptr // address alignment, used to test different alignment scenarios
|
|
||||||
wantErr bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// getVmReadStrTestCases returns test cases for vmReadStr testing
|
|
||||||
func getVmReadStrTestCases() []vmReadStrTestCase {
|
|
||||||
return []vmReadStrTestCase{
|
|
||||||
{
|
|
||||||
name: "small_buffer_aligned",
|
|
||||||
buffSize: 10,
|
|
||||||
addrAlign: 0,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "small_buffer_unaligned",
|
|
||||||
buffSize: 10,
|
|
||||||
addrAlign: 1,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "exact_page_size",
|
|
||||||
buffSize: pageSize,
|
|
||||||
addrAlign: 0,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cross_page_boundary",
|
|
||||||
buffSize: pageSize + 100,
|
|
||||||
addrAlign: uintptr(pageSize - 50),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "large_buffer_unaligned",
|
|
||||||
buffSize: pageSize * 2,
|
|
||||||
addrAlign: 123,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "buffer_smaller_than_to_boundary",
|
|
||||||
buffSize: 10,
|
|
||||||
addrAlign: uintptr(pageSize - 100), // distance to page boundary is 100 bytes, but buffer is only 10 bytes
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVmReadStr tests the vmReadStr function
|
|
||||||
func TestVmReadStr(t *testing.T) {
|
|
||||||
pid, cleanup := createTestProcess(t)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
testCases := getVmReadStrTestCases()
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
buff := make([]byte, tc.buffSize)
|
|
||||||
|
|
||||||
// Ensure the found memory region is large enough
|
|
||||||
baseAddr := findReadableMemoryRegion(t, pid, tc.buffSize)
|
|
||||||
|
|
||||||
// Use test case specified alignment offset
|
|
||||||
testAddr := baseAddr + tc.addrAlign
|
|
||||||
|
|
||||||
// Record buffer content before reading
|
|
||||||
originalBuff := make([]byte, len(buff))
|
|
||||||
copy(originalBuff, buff)
|
|
||||||
|
|
||||||
err := vmReadStr(pid, testAddr, buff)
|
|
||||||
if (err != nil) != tc.wantErr {
|
|
||||||
t.Errorf("vmReadStr() error = %v, wantErr %v", err, tc.wantErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify reading results
|
|
||||||
if !bytes.Equal(buff, originalBuff) {
|
|
||||||
// For vmReadStr, we only care that some data was read
|
|
||||||
// and that the function completed without error
|
|
||||||
t.Logf("Data was read successfully for case: %s", tc.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: check buffer size smaller than distance to boundary
|
|
||||||
if tc.name == "buffer_smaller_than_to_boundary" {
|
|
||||||
distToBoundary := pageSize - int(testAddr%uintptr(pageSize))
|
|
||||||
if distToBoundary > len(buff) {
|
|
||||||
t.Logf("Verified buffer handling when smaller than distance to boundary: dist=%d, buff=%d",
|
|
||||||
distToBoundary, len(buff))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSliceBehavior tests slice behavior
|
|
||||||
func TestSliceBehavior(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
buffSize int
|
|
||||||
nextRead int
|
|
||||||
expected int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "small_buffer_large_read",
|
|
||||||
buffSize: 10,
|
|
||||||
nextRead: 4096,
|
|
||||||
expected: 10, // must be limited to buffer size
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "large_buffer_small_read",
|
|
||||||
buffSize: 8192,
|
|
||||||
nextRead: 4096,
|
|
||||||
expected: 4096, // can use full read amount
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
buff := make([]byte, tt.buffSize)
|
|
||||||
// safely calculate actual read amount
|
|
||||||
actualRead := tt.nextRead
|
|
||||||
if tt.buffSize < actualRead {
|
|
||||||
actualRead = tt.buffSize
|
|
||||||
}
|
|
||||||
slice := buff[:actualRead]
|
|
||||||
|
|
||||||
if len(slice) != tt.expected {
|
|
||||||
t.Errorf("Expected slice len %d, got %d", tt.expected, len(slice))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
//go:build !linux
|
// +build !linux
|
||||||
|
|
||||||
package ptracer
|
package ptracer
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user