Add windows support

This commit is contained in:
criyle 2020-04-26 03:31:54 -04:00
parent f2f60ceb7c
commit 4646ce7fdc
18 changed files with 1084 additions and 107 deletions

View File

@ -125,6 +125,24 @@ The following mounts point are examples that can be configured through config fi
- pkg/envexec: run single / group of programs in parallel within restricted environment and resource constraints
- pkg/pool: reference implementation for Cgroup & Environment Pool
### Windows Support
Build `executorserver` by:
`go build ./cmd/executorserver/`
Build `executor_server.dll`: (need to install `gcc` as well)
`go build -buildmode=c-shared -o executor_server.so ./cmd/executorserver/`
Run: `./executorserver`
#### Security
- Resources are limited by [JobObject](https://docs.microsoft.com/en-us/windows/win32/procthread/job-objects)
- Privillege are limited by [Restricted Low Mandatory Level Token](https://docs.microsoft.com/en-us/windows/win32/secauthz/access-tokens)
- Low Mandatory Level directory is created for read / write
### API interface
```typescript
@ -336,6 +354,53 @@ Multiple (interaction problem):
]
```
Compile On Windows (cygwin):
```json
{
"cmd": [{
"args": ["C:\\Cygwin\\bin\\g++", "a.cc", "-o", "a"],
"env": ["PATH=C:\\Cygwin\\bin;"],
"files": [{
"content": ""
}, {
"name": "stdout",
"max": 10240
}, {
"name": "stderr",
"max": 10240
}],
"cpuLimit": 10000000000,
"memoryLimit": 104857600,
"procLimit": 50,
"copyIn": {
"a.cc": {
"content": "#include <iostream>\n#include <signal.h>\n#include <unistd.h>\nusing namespace std;\nint main() {\nint a, b;\ncin >> a >> b;\ncout << a + b << endl;\n}"
}
},
"copyOutCached": ["a.exe"]
}]
}
```
```json
[
{
"status": "Accepted",
"exitStatus": 0,
"time": 140625000,
"memory": 36286464,
"files": {
"stderr": "",
"stdout": ""
},
"fileIds": {
"a.exe": "HLQH2OF4MXUUJBCB"
}
}
]
```
## TODO
- [x] Github actions to auto build
@ -343,4 +408,6 @@ Multiple (interaction problem):
- [ ] Investigate root-free running mechanism (no cgroup && not set uid / gid)
- [ ] Investigate RLimit settings (cpu, data, fsize, stack, noFile)
- [x] Add WebSocket for job submission
- [x] Windows support
- [ ] MacOS support

View File

@ -57,7 +57,8 @@ func initEnvPool() {
printLog("Created cgroup builder with:", cgb)
cgroupPool := pool.NewFakeCgroupPool(cgb)
envPool = pool.NewEnvPool(b, cgroupPool)
builder := pool.NewEnvBuilder(b, cgroupPool)
envPool = pool.NewPool(builder)
}
type credGen struct {

View File

@ -1,4 +1,17 @@
package main
import (
"log"
"github.com/criyle/go-judge/pkg/pool"
"github.com/criyle/go-judge/pkg/winc"
)
func initEnvPool() {
b, err := winc.NewBuilder("")
if err != nil {
log.Fatalln("init container", err)
}
printLog("created winc builder")
envPool = pool.NewPool(b)
}

2
go.mod
View File

@ -10,6 +10,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 // indirect
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f
gopkg.in/yaml.v2 v2.2.8
)

4
go.sum
View File

@ -53,8 +53,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200316214734-08c61614bede h1:+jwX0O5IlL7Cj6iam25CdV2pKzCV737/4ewDRGFvwFQ=
golang.org/x/sys v0.0.0-20200316214734-08c61614bede/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE=
golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -17,7 +17,12 @@ func copyDir(src *os.File, dst string) error {
}
defer newDir.Close()
dir := src
dir, err := os.Open(src.Name())
if err != nil {
return err
}
defer dir.Close()
names, err := dir.Readdirnames(-1)
if err != nil {
return err
@ -35,13 +40,17 @@ func copyDirFile(src, dst, name string) error {
if err != nil {
return err
}
t, err := os.OpenFile(path.Join(dst, name), os.O_WRONLY, 0777)
defer s.Close()
t, err := os.OpenFile(path.Join(dst, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
if err != nil {
return err
}
defer t.Close()
_, err = io.Copy(t, s)
if err != nil {
return err
}
return nil
}
}

View File

@ -0,0 +1,42 @@
package pool
import (
"fmt"
"syscall"
"github.com/criyle/go-sandbox/container"
)
type environmentBuilder struct {
builder EnvironmentBuilder
cgPool CgroupPool
}
// NewEnvBuilder creates builder for linux container pools
func NewEnvBuilder(builder EnvironmentBuilder, cgPool CgroupPool) EnvBuilder {
return &environmentBuilder{
builder: builder,
cgPool: cgPool,
}
}
// Build creates linux container
func (b *environmentBuilder) Build() (Environment, error) {
m, err := b.builder.Build()
if err != nil {
return nil, err
}
wd, err := m.Open([]container.OpenCmd{{
Path: "/w",
Flag: syscall.O_CLOEXEC | syscall.O_DIRECTORY,
Perm: 0777,
}})
if err != nil {
return nil, fmt.Errorf("container: failed to prepare work directory")
}
return &environ{
Environment: m,
cgPool: b.cgPool,
wd: wd[0],
}, nil
}

View File

@ -10,22 +10,26 @@ import (
"github.com/criyle/go-sandbox/container"
)
var _ envexec.Environment = &Environment{}
var _ envexec.Environment = &environ{}
// Environment defines interface to access container resources
type Environment struct {
// environ defines interface to access container resources
type environ struct {
container.Environment
cgPool CgroupPool
wd *os.File // container work dir
}
// Destory destories the environment
func (c *Environment) Destory() error {
func (c *environ) Destory() error {
return c.Environment.Destroy()
}
func (c *environ) Reset() error {
return c.Environment.Reset()
}
// Execve execute process inside the environment
func (c *Environment) Execve(ctx context.Context, param envexec.ExecveParam) (envexec.Process, error) {
func (c *environ) Execve(ctx context.Context, param envexec.ExecveParam) (envexec.Process, error) {
cg, err := c.cgPool.Get()
if err != nil {
return nil, fmt.Errorf("execve: failed to get cgroup %v", err)
@ -45,13 +49,13 @@ func (c *Environment) Execve(ctx context.Context, param envexec.ExecveParam) (en
}
// WorkDir returns opened work directory, should not close after
func (c *Environment) WorkDir() *os.File {
func (c *environ) WorkDir() *os.File {
c.wd.Seek(0, 0)
return c.wd
}
// Open opens file relative to work directory
func (c *Environment) Open(path string, flags int, perm os.FileMode) (*os.File, error) {
func (c *environ) Open(path string, flags int, perm os.FileMode) (*os.File, error) {
fd, err := syscall.Openat(int(c.wd.Fd()), path, flags|syscall.O_CLOEXEC, uint32(perm))
if err != nil {
return nil, fmt.Errorf("openAtWorkDir: %v", err)

View File

@ -1,93 +0,0 @@
package pool
import (
"fmt"
"sync"
"syscall"
"github.com/criyle/go-judge/pkg/envexec"
"github.com/criyle/go-sandbox/container"
)
// EnvPool implements container environment pool
type EnvPool struct {
builder EnvironmentBuilder
cgPool CgroupPool
env []*Environment
mu sync.Mutex
}
// NewEnvPool creats new EnvPool with a builder
func NewEnvPool(builder EnvironmentBuilder, cgPool CgroupPool) *EnvPool {
return &EnvPool{
builder: builder,
cgPool: cgPool,
}
}
// Get gets environment from pool, if pool is empty, creates new environment
func (p *EnvPool) Get() (envexec.Environment, error) {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.env) > 0 {
rt := p.env[len(p.env)-1]
p.env = p.env[:len(p.env)-1]
return rt, nil
}
m, err := p.builder.Build()
if err != nil {
return nil, err
}
wd, err := m.Open([]container.OpenCmd{{
Path: "/w",
Flag: syscall.O_CLOEXEC | syscall.O_DIRECTORY,
Perm: 0777,
}})
if err != nil {
return nil, fmt.Errorf("container: failed to prepare work directory")
}
return &Environment{
Environment: m,
cgPool: p.cgPool,
wd: wd[0],
}, nil
}
// Put puts environment to the pool with reset the environment
func (p *EnvPool) Put(e envexec.Environment) {
env, _ := e.(*Environment)
env.Reset()
p.mu.Lock()
defer p.mu.Unlock()
p.env = append(p.env, env)
}
// Destroy destory an environment
func (p *EnvPool) Destroy(e envexec.Environment) {
env, _ := e.(*Environment)
env.Destroy()
}
// Release clears the pool
func (p *EnvPool) Release() {
p.mu.Lock()
defer p.mu.Unlock()
for _, e := range p.env {
p.Destroy(e)
}
}
// Shutdown indicates a shutdown event, not implemented yes
func (p *EnvPool) Shutdown() {
p.mu.Lock()
defer p.mu.Unlock()
for _, e := range p.env {
p.Destroy(e)
}
}

55
pkg/pool/interface.go Normal file
View File

@ -0,0 +1,55 @@
package pool
import (
"sync"
"github.com/criyle/go-judge/pkg/envexec"
)
// Environment defines envexec.Environment with destroy
type Environment interface {
envexec.Environment
Reset() error
Destroy() error
}
// EnvBuilder defines the abstract builder for container environment
type EnvBuilder interface {
Build() (Environment, error)
}
type pool struct {
builder EnvBuilder
env []Environment
mu sync.Mutex
}
// NewPool returns a pool for EnvBuilder
func NewPool(builder EnvBuilder) envexec.EnvironmentPool {
return &pool{
builder: builder,
}
}
func (p *pool) Get() (envexec.Environment, error) {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.env) > 0 {
rt := p.env[len(p.env)-1]
p.env = p.env[:len(p.env)-1]
return rt, nil
}
return p.builder.Build()
}
func (p *pool) Put(env envexec.Environment) {
e, _ := env.(Environment)
e.Reset()
p.mu.Lock()
defer p.mu.Unlock()
p.env = append(p.env, e)
}

View File

@ -0,0 +1,74 @@
package winc
import (
"io/ioutil"
"os"
"github.com/criyle/go-judge/pkg/pool"
"golang.org/x/sys/windows"
)
const sdString = "S:(ML;;NW;;;LW)D:(A;;0x12019f;;;WD)"
const wdPrefix = "es"
const tmpPrefix = "es_tmp"
var _ pool.EnvBuilder = &builder{}
type builder struct {
root string
token windows.Token
sd *windows.SECURITY_DESCRIPTOR
}
// NewBuilder returns a builder for windows container environment
func NewBuilder(root string) (pool.EnvBuilder, error) {
sd, err := windows.SecurityDescriptorFromString(sdString)
if err != nil {
return nil, err
}
token, err := createLowMandatoryLevelToken()
if err != nil {
return nil, err
}
return &builder{
root: root,
token: token,
sd: sd,
}, nil
}
func (b *builder) Build() (pool.Environment, error) {
sacl, _, err := b.sd.SACL()
if err != nil {
return nil, err
}
workDir, err := ioutil.TempDir(b.root, wdPrefix)
if err != nil {
return nil, err
}
if err := windows.SetNamedSecurityInfo(workDir, windows.SE_FILE_OBJECT,
windows.LABEL_SECURITY_INFORMATION, nil, nil, nil, sacl); err != nil {
return nil, err
}
tmpDir, err := ioutil.TempDir(b.root, tmpPrefix)
if err != nil {
return nil, err
}
if err := windows.SetNamedSecurityInfo(tmpDir, windows.SE_FILE_OBJECT,
windows.LABEL_SECURITY_INFORMATION, nil, nil, nil, sacl); err != nil {
return nil, err
}
wd, err := os.Open(workDir)
if err != nil {
return nil, err
}
return &Environment{
root: workDir,
tmp: tmpDir,
wd: wd,
token: b.token,
sd: b.sd,
}, nil
}

View File

@ -0,0 +1,409 @@
package winc
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"os"
"path"
"syscall"
"unicode/utf16"
"unsafe"
"github.com/criyle/go-judge/pkg/envexec"
"github.com/criyle/go-judge/pkg/pool"
"github.com/criyle/go-sandbox/runner"
"golang.org/x/sys/windows"
)
var _ pool.Environment = &Environment{}
var (
errFileCount = errors.New("windows requires std handle to be 3")
errArgs = errors.New("executable name is required")
)
// Environment implements envexec.Environment interface
type Environment struct {
root string
tmp string
wd *os.File
token windows.Token
sd *windows.SECURITY_DESCRIPTOR
}
// Execve implements windows sandbox ..
func (e *Environment) Execve(ctx context.Context, param envexec.ExecveParam) (proc envexec.Process, err error) {
if len(param.Files) != 3 {
return nil, errFileCount
}
if len(param.Args) == 0 {
return nil, errArgs
}
argv0, err := joinExeDirAndFName(e.root, param.Args[0])
if err != nil {
return nil, err
}
param.Args[0] = argv0
// cleanUp defines clean up functions to call when error happens
var cleanUp []func()
handle := func(f func()) {
cleanUp = append(cleanUp, f)
}
defer func() {
if err != nil {
for i := len(cleanUp) - 1; i >= 0; i-- {
cleanUp[i]()
}
}
}()
// create desktop
random := make([]byte, 8)
if _, err := rand.Read(random); err != nil {
return nil, err
}
deskName := fmt.Sprintf("winc_%08x_%s", windows.GetCurrentProcessId(), hex.EncodeToString(random))
deskNameW := syscall.StringToUTF16Ptr(deskName)
sa := windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: e.sd,
}
newDesk, err := CreateDesktop(deskNameW, nil, 0, 0, GENERIC_ALL, &sa)
if err != nil {
return nil, err
}
handle(func() { CloseDesktop(newDesk) })
// create job object
hJob, err := createJobObject(param.Limit)
if err != nil {
return nil, err
}
handle(func() { windows.CloseHandle(hJob) })
// create IOCP
ioPort, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 1)
if err != nil {
return nil, err
}
handle(func() { windows.CloseHandle(ioPort) })
completionPort := &JOBOBJECT_ASSOCIATE_COMPLETION_PORT{
CompletionKey: hJob,
CompletionPort: ioPort,
}
if _, err := windows.SetInformationJobObject(hJob, windows.JobObjectAssociateCompletionPortInformation,
uintptr(unsafe.Pointer(completionPort)), uint32(unsafe.Sizeof(*completionPort))); err != nil {
return nil, err
}
cmdLine := makeCmdLine(param.Args)
cmdLineW := syscall.StringToUTF16Ptr(cmdLine)
dirW := syscall.StringToUTF16Ptr(e.root)
var startupInfo syscall.StartupInfo
startupInfo.Cb = uint32(unsafe.Sizeof(startupInfo))
startupInfo.Flags |= windows.STARTF_USESTDHANDLES // STARTF_FORCEOFFFEEDBACK
startupInfo.Desktop = deskNameW
var env []string
env = append(env, param.Env...)
env = append(env, "TMP="+e.tmp)
env = append(env, "TEMP="+e.tmp)
env = append(env, "LocalAppData="+e.tmp)
eb := createEnvBlock(env)
// do create process
var processInfo syscall.ProcessInformation
err = func() error {
syscall.ForkLock.Lock()
defer syscall.ForkLock.Unlock()
curProc := windows.CurrentProcess()
fd := make([]windows.Handle, len(param.Files))
for i := range param.Files {
if param.Files[i] <= 0 {
continue
}
if err := windows.DuplicateHandle(curProc, windows.Handle(param.Files[i]),
curProc, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS); err != nil {
return err
}
defer windows.CloseHandle(fd[i])
}
startupInfo.StdInput = syscall.Handle(fd[0])
startupInfo.StdOutput = syscall.Handle(fd[1])
startupInfo.StdErr = syscall.Handle(fd[2])
return syscall.CreateProcessAsUser(syscall.Token(e.token), nil, cmdLineW, nil, nil, true,
windows.CREATE_NEW_PROCESS_GROUP|windows.CREATE_NEW_CONSOLE|
windows.CREATE_SUSPENDED|windows.CREATE_UNICODE_ENVIRONMENT,
eb, dirW, &startupInfo, &processInfo)
}()
if processInfo.Process > 0 {
handle(func() { syscall.CloseHandle(processInfo.Process) })
}
if processInfo.Thread > 0 {
handle(func() { syscall.CloseHandle(processInfo.Thread) })
}
if err != nil {
return nil, err
}
// assign process to job object
if err := windows.AssignProcessToJobObject(hJob, windows.Handle(processInfo.Process)); err != nil {
return nil, err
}
// resume thread
if _, err := windows.ResumeThread(windows.Handle(processInfo.Thread)); err != nil {
return nil, err
}
syscall.CloseHandle(processInfo.Thread)
done := make(chan struct{})
procSet := &process{
done: done,
hProcess: windows.Handle(processInfo.Process),
hJob: hJob,
}
// wait for ctx to terminate
go func() {
select {
case <-ctx.Done():
windows.TerminateJobObject(hJob, 1)
case <-done:
}
}()
// wait for job object to finish
go func() {
defer CloseDesktop(newDesk)
defer syscall.CloseHandle(processInfo.Process)
defer windows.CloseHandle(hJob)
defer windows.CloseHandle(ioPort)
defer close(done)
var (
qty, key uint32
overlapped *windows.Overlapped
)
result := runner.Result{
Status: runner.StatusNormal,
}
loop:
for {
err = windows.GetQueuedCompletionStatus(ioPort, &qty, &key, &overlapped, windows.INFINITE)
if err != nil {
procSet.result = runner.Result{Status: runner.StatusRunnerError, Error: err.Error()}
return
}
switch qty {
case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO:
break loop
case JOB_OBJECT_MSG_END_OF_JOB_TIME, JOB_OBJECT_MSG_END_OF_PROCESS_TIME:
result.Status = runner.StatusTimeLimitExceeded
windows.TerminateJobObject(hJob, 0)
case JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT:
result.Status = runner.StatusMemoryLimitExceeded
windows.TerminateJobObject(hJob, 0)
// case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:
// result.Status = runner.StatusNonzeroExitStatus
// windows.TerminateJobObject(hJob, 0)
case JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT, JOB_OBJECT_MSG_JOB_MEMORY_LIMIT:
result.Status = runner.StatusMemoryLimitExceeded
windows.TerminateJobObject(hJob, 0)
// JOB_OBJECT_MSG_NEW_PROCESS, JOB_OBJECT_MSG_EXIT_PROCESS
}
}
// collect exit status
if _, err := windows.WaitForSingleObject(windows.Handle(processInfo.Process), windows.INFINITE); err != nil {
procSet.result = runner.Result{Status: runner.StatusRunnerError, Error: err.Error()}
return
}
var exitCode uint32
if err := windows.GetExitCodeProcess(windows.Handle(processInfo.Process), &exitCode); err != nil {
procSet.result = runner.Result{Status: runner.StatusRunnerError, Error: err.Error()}
return
}
if exitCode != 0 {
result.Status = runner.StatusNonzeroExitStatus
}
result.ExitStatus = int(exitCode)
// collect usage
t, m, err := getJobOjbectUsage(hJob)
if err != nil {
procSet.result = runner.Result{Status: runner.StatusRunnerError, Error: err.Error()}
return
}
result.Time = t
result.Memory = m
procSet.result = result
}()
return procSet, nil
}
// WorkDir returns the work directory
func (e *Environment) WorkDir() *os.File {
e.wd.Seek(0, 0)
return e.wd
}
// Open opens file related to root
func (e *Environment) Open(p string, flags int, perm os.FileMode) (*os.File, error) {
return os.OpenFile(path.Join(e.root, p), flags, perm)
}
// Destroy destorys the environment
func (e *Environment) Destroy() error {
return e.wd.Close()
}
// Reset remove all files in root directory
func (e *Environment) Reset() error {
return removeContents(e.root)
}
// removeContents delete content of a directory
func removeContents(dir string) error {
d, err := os.Open(dir)
if err != nil {
return err
}
defer d.Close()
names, err := d.Readdirnames(-1)
if err != nil {
return err
}
for _, name := range names {
err = os.RemoveAll(path.Join(dir, name))
if err != nil {
return err
}
}
return nil
}
// From syscall/exec_windows.go
// makeCmdLine builds a command line out of args by escaping "special"
// characters and joining the arguments with spaces.
func makeCmdLine(args []string) string {
var s string
for _, v := range args {
if s != "" {
s += " "
}
s += syscall.EscapeArg(v)
}
return s
}
// createEnvBlock converts an array of environment strings into
// the representation required by CreateProcess: a sequence of NUL
// terminated strings followed by a nil.
// Last bytes are two UCS-2 NULs, or four NUL bytes.
func createEnvBlock(envv []string) *uint16 {
if len(envv) == 0 {
return &utf16.Encode([]rune("\x00\x00"))[0]
}
length := 0
for _, s := range envv {
length += len(s) + 1
}
length++
b := make([]byte, length)
i := 0
for _, s := range envv {
l := len(s)
copy(b[i:i+l], []byte(s))
copy(b[i+l:i+l+1], []byte{0})
i = i + l + 1
}
copy(b[i:i+1], []byte{0})
return &utf16.Encode([]rune(string(b)))[0]
}
func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}
func normalizeDir(dir string) (name string, err error) {
ndir, err := syscall.FullPath(dir)
if err != nil {
return "", err
}
if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) {
// dir cannot have \\server\share\path form
return "", syscall.EINVAL
}
return ndir, nil
}
func volToUpper(ch int) int {
if 'a' <= ch && ch <= 'z' {
ch += 'A' - 'a'
}
return ch
}
func joinExeDirAndFName(dir, p string) (name string, err error) {
if len(p) == 0 {
return "", syscall.EINVAL
}
if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) {
// \\server\share\path form
return p, nil
}
if len(p) > 1 && p[1] == ':' {
// has drive letter
if len(p) == 2 {
return "", syscall.EINVAL
}
if isSlash(p[2]) {
return p, nil
}
d, err := normalizeDir(dir)
if err != nil {
return "", err
}
if volToUpper(int(p[0])) == volToUpper(int(d[0])) {
return syscall.FullPath(d + "\\" + p[2:])
}
return syscall.FullPath(p)
}
// no drive letter
d, err := normalizeDir(dir)
if err != nil {
return "", err
}
if isSlash(p[0]) {
return syscall.FullPath(d[:2] + p)
}
return syscall.FullPath(d + "\\" + p)
}

View File

@ -0,0 +1,64 @@
package winc
import (
"time"
"unsafe"
"github.com/criyle/go-judge/pkg/envexec"
"golang.org/x/sys/windows"
)
func createJobObject(envLimit envexec.Limit) (windows.Handle, error) {
// create job object
hJob, err := windows.CreateJobObject(nil, nil)
if err != nil {
return 0, err
}
// define job object limits
var limit windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
// time limits: 100 nanoseconds / unit
if envLimit.Time > 0 {
limit.BasicLimitInformation.LimitFlags |= windows.JOB_OBJECT_LIMIT_JOB_TIME
limit.BasicLimitInformation.PerJobUserTimeLimit = int64(envLimit.Time.Round(100*time.Nanosecond).Nanoseconds() / 100)
}
// memory limits: byte
if envLimit.Memory > 0 {
limit.BasicLimitInformation.LimitFlags |= windows.JOB_OBJECT_LIMIT_JOB_MEMORY
limit.JobMemoryLimit = uintptr(envLimit.Memory)
}
// process limit
if envLimit.Proc > 0 {
limit.BasicLimitInformation.LimitFlags |= windows.JOB_OBJECT_LIMIT_ACTIVE_PROCESS
limit.BasicLimitInformation.ActiveProcessLimit = uint32(envLimit.Proc)
}
// additional limitations
limit.BasicLimitInformation.LimitFlags |= windows.JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION | windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
_, err = windows.SetInformationJobObject(hJob, windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limit)), uint32(unsafe.Sizeof(limit)))
if err != nil {
windows.CloseHandle(hJob)
return 0, err
}
// ui restrictions
var uiRestriction windows.JOBOBJECT_BASIC_UI_RESTRICTIONS
uiRestriction.UIRestrictionsClass = windows.JOB_OBJECT_UILIMIT_EXITWINDOWS |
windows.JOB_OBJECT_UILIMIT_DESKTOP |
windows.JOB_OBJECT_UILIMIT_DISPLAYSETTINGS |
windows.JOB_OBJECT_UILIMIT_GLOBALATOMS |
windows.JOB_OBJECT_UILIMIT_HANDLES |
windows.JOB_OBJECT_UILIMIT_READCLIPBOARD |
windows.JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS |
windows.JOB_OBJECT_UILIMIT_WRITECLIPBOARD
_, err = windows.SetInformationJobObject(hJob, windows.JobObjectBasicUIRestrictions, uintptr(unsafe.Pointer(&uiRestriction)), uint32(unsafe.Sizeof(uiRestriction)))
if err != nil {
windows.CloseHandle(hJob)
return 0, err
}
return hJob, nil
}

3
pkg/winc/mksyscall.go Normal file
View File

@ -0,0 +1,3 @@
package winc
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go

View File

@ -0,0 +1,53 @@
package winc
import (
"time"
"unsafe"
"github.com/criyle/go-judge/pkg/envexec"
"github.com/criyle/go-sandbox/runner"
"golang.org/x/sys/windows"
)
var _ envexec.Process = &process{}
type process struct {
done chan struct{}
result runner.Result
hJob windows.Handle
hProcess windows.Handle
}
func (p *process) Done() <-chan struct{} {
return p.done
}
func (p *process) Result() runner.Result {
<-p.done
return p.result
}
func (p *process) Usage() envexec.Usage {
t, m, _ := getJobOjbectUsage(p.hJob)
return envexec.Usage{
Time: t,
Memory: m,
}
}
func getJobOjbectUsage(hJob windows.Handle) (time.Duration, runner.Size, error) {
basicInfo := new(JOBOBJECT_BASIC_ACCOUNTING_INFORMATION)
if _, err := QueryInformationJobObject(hJob, JobObjectBasicAccountingInformation,
uintptr(unsafe.Pointer(basicInfo)), uint32(unsafe.Sizeof(*basicInfo)), nil); err != nil {
return 0, 0, err
}
t := time.Duration(basicInfo.TotalUserTime) * 100 // 100 nanosecond tick
extendedLimitInfo := new(windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION)
if _, err := QueryInformationJobObject(hJob, windows.JobObjectExtendedLimitInformation,
uintptr(unsafe.Pointer(extendedLimitInfo)), uint32(unsafe.Sizeof(*extendedLimitInfo)), nil); err != nil {
return 0, 0, err
}
m := runner.Size(extendedLimitInfo.PeakJobMemoryUsed)
return t, m, nil
}

121
pkg/winc/syscall_windows.go Normal file
View File

@ -0,0 +1,121 @@
package winc
import "golang.org/x/sys/windows"
// JOBOBJECT_ASSOCIATE_COMPLETION_PORT defines IOCP for job object
type JOBOBJECT_ASSOCIATE_COMPLETION_PORT struct {
CompletionKey windows.Handle
CompletionPort windows.Handle
}
// JOBOBJECT_BASIC_ACCOUNTING_INFORMATION defines basic accounting information for a job object
type JOBOBJECT_BASIC_ACCOUNTING_INFORMATION struct {
TotalUserTime int64
TotalKernelTime int64
ThisPeriodTotalUserTime int64
ThisPeriodTotalKernelTime int64
TotalPageFaultCount uint32
TotalProcesses uint32
ActiveProcesses uint32
TotalTerminatedProcess uint32
}
// JobObject Information class
const (
JobObjectBasicAccountingInformation = iota + 1
JobObjectBasicLimitInformation
JobObjectBasicProcessIDList
JobObjectBasicUIRestrictions
JobObjectSecurityLimitInformation
JobObjectEndOfJobTimeInformation
JobObjectAssociateCompletionPortInformation
JobObjectBasicAndIoAccountingInformation
JobObjectExtendedLimitInformation
_
JobObjectGroupInformation
JobObjectNotificationLimitInformation
JobObjectLimitViolationInformation
JobObjectGroupInformationEx
JobObjectCPURateControlInformation
)
// JobObject Messages https://chromium.googlesource.com/chromium/deps/mozprocess/+/11d11bebc8517dcedec71f377cbec07fb91a3b1f/mozprocess/winprocess.py
// https://docs.microsoft.com/en-ca/windows/win32/api/winnt/ns-winnt-jobobject_associate_completion_port?redirectedfrom=MSDN
const (
JOB_OBJECT_MSG_END_OF_JOB_TIME = iota + 1
JOB_OBJECT_MSG_END_OF_PROCESS_TIME
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO
JOB_OBJECT_MSG_NEW_PROCESS
JOB_OBJECT_MSG_EXIT_PROCESS
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT
)
// https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken
// CreateRestrictedToken
// CreateRestrictedToken Flags
const (
DISABLE_MAX_PRIVILEGE = 1 << iota
SANDBOX_INERT
LUA_TOKEN
WRITE_RESTRICTED
)
// https://docs.microsoft.com/en-ca/windows/win32/winstation/desktop-security-and-access-rights
const (
DESKTOP_READOBJECTS windows.ACCESS_MASK = 1 << iota
DESKTOP_CREATEWINDOW
DESKTOP_CREATEMENU
DESKTOP_HOOKCONTROL
DESKTOP_JOURNALRECORD
DESKTOP_JOURNALPLAYBACK
DESKTOP_ENUMERATE
DESKTOP_WRITEOBJECTS
DESKTOP_SWITCHDESKTOP // 0x0100L
)
// permissions
const (
DELETE windows.ACCESS_MASK = 1 << (iota + 16)
READ_CONTROL
WRITE_DAC
WRITE_OWNER
SYNCHRONIZE
)
// https://docs.microsoft.com/en-us/windows/win32/secauthz/standard-access-rights
const (
GENERIC_READ = DESKTOP_ENUMERATE | DESKTOP_READOBJECTS | READ_CONTROL
GENERIC_WRITE = DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW |
DESKTOP_HOOKCONTROL | DESKTOP_JOURNALPLAYBACK | DESKTOP_JOURNALRECORD |
DESKTOP_WRITEOBJECTS | READ_CONTROL
GENERIC_EXECUTE = DESKTOP_SWITCHDESKTOP | READ_CONTROL
GENERIC_ALL = DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW |
DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL | DESKTOP_JOURNALPLAYBACK |
DESKTOP_JOURNALRECORD | DESKTOP_READOBJECTS | DESKTOP_SWITCHDESKTOP |
DESKTOP_WRITEOBJECTS | READ_CONTROL | WRITE_DAC | WRITE_OWNER
)
// HDESK is handle for desktop
type HDESK windows.Handle
// HWINSTA is handle for windows station
type HWINSTA windows.Handle
// Mandatory Level Sids
const (
SID_SYSTEM_MANDATORY_LEVEL = "S-1-16-16384"
SID_HIGH_MANDATORY_LEVEL = "S-1-16-12288"
SID_MEDIUM_MANDATORY_LEVEL = "S-1-16-8192"
SID_LOW_MANDATORY_LEVEL = "S-1-16-4096"
SID_UNTRUSTED_MANDATORY_LEVEL = "S-1-16-0"
)
//sys CreateRestrictedToken(existingToken windows.Token, flags uint32, disableSidCount uint32, sidsToDisable *windows.SIDAndAttributes, deletePrivilegeCount uint32, privilegesToDelete *windows.SIDAndAttributes, restrictedSidCount uint32, sidToRestrict *windows.SIDAndAttributes, newTokenHandle *windows.Token) (err error) = advapi32.CreateRestrictedToken
//sys GetThreadDesktop(threadID uint32) (h HDESK) = user32.GetThreadDesktop
//sys GetProcessWindowStation() (h HWINSTA) = user32.GetProcessWindowStation
//sys CreateDesktop(lpszDesktop *uint16, lpszDevice *uint16, pDevmode uintptr, dwFlags uint32, dwDesiredAccess windows.ACCESS_MASK, lpsa *windows.SecurityAttributes) (h HDESK, err error) = user32.CreateDesktopW
//sys CloseDesktop(hDesktop HDESK) (err error) = user32.CloseDesktop
//sys QueryInformationJobObject(job windows.Handle, JobObjectInformationClass uint32, JobObjectInformation uintptr, JobObjectInformationLength uint32, lpReturnLength *uint32) (ret int, err error) = kernel32.QueryInformationJobObject

43
pkg/winc/token_windows.go Normal file
View File

@ -0,0 +1,43 @@
package winc
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
func createLowMandatoryLevelToken() (token windows.Token, err error) {
// Get current process token
var procToken windows.Token
if err := windows.OpenProcessToken(windows.CurrentProcess(),
windows.TOKEN_QUERY|windows.TOKEN_DUPLICATE|windows.TOKEN_ADJUST_DEFAULT|windows.TOKEN_ASSIGN_PRIMARY,
&procToken); err != nil {
return token, err
}
// create restricted token
if err := CreateRestrictedToken(procToken, DISABLE_MAX_PRIVILEGE, 0, nil, 0, nil, 0, nil, &token); err != nil {
return token, err
}
defer func() {
if err != nil {
token.Close()
}
}()
lowSid, err := windows.StringToSid(SID_LOW_MANDATORY_LEVEL)
if err != nil {
return token, err
}
tml := windows.Tokenmandatorylabel{
Label: windows.SIDAndAttributes{
Sid: lowSid,
Attributes: windows.SE_GROUP_INTEGRITY,
},
}
if err = windows.SetTokenInformation(token, syscall.TokenIntegrityLevel, (*byte)(unsafe.Pointer(&tml)), uint32(unsafe.Sizeof(tml))); err != nil {
return token, err
}
return token, nil
}

View File

@ -0,0 +1,112 @@
// Code generated by 'go generate'; DO NOT EDIT.
package winc
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
// Do the interface allocations only once for common
// Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var (
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
)
// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return nil
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
var (
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
moduser32 = windows.NewLazySystemDLL("user32.dll")
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procCreateRestrictedToken = modadvapi32.NewProc("CreateRestrictedToken")
procGetThreadDesktop = moduser32.NewProc("GetThreadDesktop")
procGetProcessWindowStation = moduser32.NewProc("GetProcessWindowStation")
procCreateDesktopW = moduser32.NewProc("CreateDesktopW")
procCloseDesktop = moduser32.NewProc("CloseDesktop")
procQueryInformationJobObject = modkernel32.NewProc("QueryInformationJobObject")
)
func CreateRestrictedToken(existingToken windows.Token, flags uint32, disableSidCount uint32, sidsToDisable *windows.SIDAndAttributes, deletePrivilegeCount uint32, privilegesToDelete *windows.SIDAndAttributes, restrictedSidCount uint32, sidToRestrict *windows.SIDAndAttributes, newTokenHandle *windows.Token) (err error) {
r1, _, e1 := syscall.Syscall9(procCreateRestrictedToken.Addr(), 9, uintptr(existingToken), uintptr(flags), uintptr(disableSidCount), uintptr(unsafe.Pointer(sidsToDisable)), uintptr(deletePrivilegeCount), uintptr(unsafe.Pointer(privilegesToDelete)), uintptr(restrictedSidCount), uintptr(unsafe.Pointer(sidToRestrict)), uintptr(unsafe.Pointer(newTokenHandle)))
if r1 == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func GetThreadDesktop(threadID uint32) (h HDESK) {
r0, _, _ := syscall.Syscall(procGetThreadDesktop.Addr(), 1, uintptr(threadID), 0, 0)
h = HDESK(r0)
return
}
func GetProcessWindowStation() (h HWINSTA) {
r0, _, _ := syscall.Syscall(procGetProcessWindowStation.Addr(), 0, 0, 0, 0)
h = HWINSTA(r0)
return
}
func CreateDesktop(lpszDesktop *uint16, lpszDevice *uint16, pDevmode uintptr, dwFlags uint32, dwDesiredAccess windows.ACCESS_MASK, lpsa *windows.SecurityAttributes) (h HDESK, err error) {
r0, _, e1 := syscall.Syscall6(procCreateDesktopW.Addr(), 6, uintptr(unsafe.Pointer(lpszDesktop)), uintptr(unsafe.Pointer(lpszDevice)), uintptr(pDevmode), uintptr(dwFlags), uintptr(dwDesiredAccess), uintptr(unsafe.Pointer(lpsa)))
h = HDESK(r0)
if h == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func CloseDesktop(hDesktop HDESK) (err error) {
r1, _, e1 := syscall.Syscall(procCloseDesktop.Addr(), 1, uintptr(hDesktop), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func QueryInformationJobObject(job windows.Handle, JobObjectInformationClass uint32, JobObjectInformation uintptr, JobObjectInformationLength uint32, lpReturnLength *uint32) (ret int, err error) {
r0, _, e1 := syscall.Syscall6(procQueryInformationJobObject.Addr(), 5, uintptr(job), uintptr(JobObjectInformationClass), uintptr(JobObjectInformation), uintptr(JobObjectInformationLength), uintptr(unsafe.Pointer(lpReturnLength)), 0)
ret = int(r0)
if ret == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = syscall.EINVAL
}
}
return
}