diff --git a/README.md b/README.md index bda4248..52da588 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ DifySandbox currently only supports Linux, as it's designed for docker container 1. Clone the repository using `git clone https://github.com/langgenius/dify-sandbox` and navigate to the project directory. 2. Run ./install.sh to install the necessary dependencies. 3. Run ./build/build_[amd64|arm64].sh to build the sandbox binary. -4. Run ./main to start the server. +4. Run ./sandbox_userctl.sh to create sandbox-related users, it needs to be executed by the root user +5. Run ./main to start the server. If you want to debug the server, firstly use build script to build the sandbox library binaries, then debug as you want with your IDE. diff --git a/cmd/lib/nodejs/main.go b/cmd/lib/nodejs/main.go index 07e3086..8d43422 100644 --- a/cmd/lib/nodejs/main.go +++ b/cmd/lib/nodejs/main.go @@ -1,13 +1,29 @@ package main -import "github.com/langgenius/dify-sandbox/internal/core/lib/nodejs" +import ( + "fmt" + "os" + + "github.com/langgenius/dify-sandbox/internal/core/lib" + "github.com/langgenius/dify-sandbox/internal/core/lib/nodejs" +) + +/* +#include +*/ import "C" //export DifySeccomp -func DifySeccomp(uid int, gid int, enable_network bool) { - if err := nodejs.InitSeccomp(uid, gid, enable_network); err != nil { - panic(err) +func DifySeccomp(uid int, gid int, enable_network bool) C.int { + err := nodejs.InitSeccomp(uid, gid, enable_network) + if err != nil { + fmt.Fprintf(os.Stderr, "nodejs DifySeccomp error: %v\n", err) + if coder, ok := err.(lib.ErrorCoder); ok { + return C.int(coder.GetCode()) + } + return C.int(lib.ERR_UNKNOWN) } + return C.int(lib.SUCCESS) } func main() {} diff --git a/cmd/lib/python/main.go b/cmd/lib/python/main.go index aa968d6..f7b2127 100644 --- a/cmd/lib/python/main.go +++ b/cmd/lib/python/main.go @@ -1,15 +1,29 @@ package main import ( + "fmt" + "os" + + "github.com/langgenius/dify-sandbox/internal/core/lib" "github.com/langgenius/dify-sandbox/internal/core/lib/python" ) + +/* +#include +*/ import "C" //export DifySeccomp -func DifySeccomp(uid int, gid int, enable_network bool) { - if err := python.InitSeccomp(uid, gid, enable_network); err != nil { - panic(err) +func DifySeccomp(uid int, gid int, enable_network bool) C.int { + err := python.InitSeccomp(uid, gid, enable_network) + if err != nil { + fmt.Fprintf(os.Stderr, "python DifySeccomp error: %v\n", err) + if coder, ok := err.(lib.ErrorCoder); ok { + return C.int(coder.GetCode()) + } + return C.int(lib.ERR_UNKNOWN) } + return C.int(lib.SUCCESS) } func main() {} diff --git a/internal/core/lib/const.go b/internal/core/lib/const.go new file mode 100644 index 0000000..39e0682 --- /dev/null +++ b/internal/core/lib/const.go @@ -0,0 +1,13 @@ +package lib + +const ( + SUCCESS = 0 + ERR_CHROOT = 1 + ERR_CHDIR = 2 + ERR_SETNONEWPRIVS = 3 + ERR_SECCOMP = 4 + ERR_SETUID = 5 + ERR_SETGID = 6 + ERR_SETGROPS = 7 + ERR_UNKNOWN = 99 +) diff --git a/internal/core/lib/error_type.go b/internal/core/lib/error_type.go new file mode 100644 index 0000000..01613c9 --- /dev/null +++ b/internal/core/lib/error_type.go @@ -0,0 +1,82 @@ +package lib + +import ( + "fmt" +) + +type ErrorCoder interface { + GetCode() int +} + +type BaseError struct { + Err error + Code int +} + +func (e *BaseError) Error() string { + return fmt.Sprintf("error code %d: %v", e.Code, e.Err) +} + +func (e *BaseError) Unwrap() error { + return e.Err +} + +func (e *BaseError) GetCode() int { + return e.Code +} + +type ChrootError struct { + BaseError +} + +func (e *ChrootError) Error() string { + return fmt.Sprintf("chroot failed: %v", e.Err) +} + +type ChdirError struct { + BaseError +} + +func (e *ChdirError) Error() string { + return fmt.Sprintf("chdir failed: %v", e.Err) +} + +type SetNoNewPrivsError struct { + BaseError +} + +func (e *SetNoNewPrivsError) Error() string { + return fmt.Sprintf("set no new privs failed: %v", e.Err) +} + +type SetuidError struct { + BaseError +} + +func (e *SetuidError) Error() string { + return fmt.Sprintf("setuid failed: %v", e.Err) +} + +type SetgidError struct { + BaseError +} + +func (e *SetgidError) Error() string { + return fmt.Sprintf("setgid failed: %v", e.Err) +} + +type SeccompError struct { + BaseError +} + +func (e *SeccompError) Error() string { + return fmt.Sprintf("seccomp failed: %v", e.Err) +} + +type SetgroupsError struct { + BaseError +} + +func (e *SetgroupsError) Error() string { + return fmt.Sprintf("setgroups failed: %v", e.Err) +} diff --git a/internal/core/lib/nodejs/add_seccomp.go b/internal/core/lib/nodejs/add_seccomp.go index 4e6184a..27bead1 100644 --- a/internal/core/lib/nodejs/add_seccomp.go +++ b/internal/core/lib/nodejs/add_seccomp.go @@ -17,14 +17,32 @@ import ( func InitSeccomp(uid int, gid int, enable_network bool) error { err := syscall.Chroot(".") if err != nil { - return err + return &lib.ChrootError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_CHROOT, + }, + } } err = syscall.Chdir("/") if err != nil { - return err + return &lib.ChdirError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_CHDIR, + }, + } } - lib.SetNoNewPrivs() + err = lib.SetNoNewPrivs() + if err != nil { + return &lib.SetNoNewPrivsError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SETNONEWPRIVS, + }, + } + } allowed_syscalls := []int{} allowed_not_kill_syscalls := []int{} @@ -51,24 +69,44 @@ func InitSeccomp(uid int, gid int, enable_network bool) error { err = lib.Seccomp(allowed_syscalls, allowed_not_kill_syscalls) if err != nil { - return err + return &lib.SeccompError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SECCOMP, + }, + } } err = syscall.Setgroups([]int{}) if err != nil { - return err + return &lib.SetgroupsError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SETGROPS, + }, + } } // setgid err = syscall.Setgid(gid) if err != nil { - return err + return &lib.SetgidError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SETGID, + }, + } } // setuid err = syscall.Setuid(uid) if err != nil { - return err + return &lib.SetuidError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SETUID, + }, + } } return nil diff --git a/internal/core/lib/python/add_seccomp.go b/internal/core/lib/python/add_seccomp.go index 0b04289..8f40819 100644 --- a/internal/core/lib/python/add_seccomp.go +++ b/internal/core/lib/python/add_seccomp.go @@ -17,14 +17,32 @@ import ( func InitSeccomp(uid int, gid int, enable_network bool) error { err := syscall.Chroot(".") if err != nil { - return err + return &lib.ChrootError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_CHROOT, + }, + } } err = syscall.Chdir("/") if err != nil { - return err + return &lib.ChdirError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_CHDIR, + }, + } } - lib.SetNoNewPrivs() + err = lib.SetNoNewPrivs() + if err != nil { + return &lib.SetNoNewPrivsError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SETNONEWPRIVS, + }, + } + } allowed_syscalls := []int{} allowed_not_kill_syscalls := []int{} @@ -50,24 +68,44 @@ func InitSeccomp(uid int, gid int, enable_network bool) error { err = lib.Seccomp(allowed_syscalls, allowed_not_kill_syscalls) if err != nil { - return err + return &lib.SeccompError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SECCOMP, + }, + } } err = syscall.Setgroups([]int{}) if err != nil { - return err + return &lib.SetgroupsError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SETGROPS, + }, + } } // setgid err = syscall.Setgid(gid) if err != nil { - return err + return &lib.SetgidError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SETGID, + }, + } } // setuid err = syscall.Setuid(uid) if err != nil { - return err + return &lib.SetuidError{ + BaseError: lib.BaseError{ + Err: err, + Code: lib.ERR_SETUID, + }, + } } return nil diff --git a/internal/core/runner/nodejs/nodejs.h b/internal/core/runner/nodejs/nodejs.h index 85cb3e9..2546364 100644 --- a/internal/core/runner/nodejs/nodejs.h +++ b/internal/core/runner/nodejs/nodejs.h @@ -21,6 +21,11 @@ extern const char *_GoStringPtr(_GoString_ s); /* Start of preamble from import "C" comments. */ +#line 11 "main.go" + +#include + +#line 1 "cgo-generated-wrapper" /* End of preamble from import "C" comments. */ @@ -82,7 +87,7 @@ typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; extern "C" { #endif -extern void DifySeccomp(GoInt uid, GoInt gid, GoUint8 enable_network); +extern int DifySeccomp(GoInt uid, GoInt gid, GoUint8 enable_network); #ifdef __cplusplus } diff --git a/internal/core/runner/nodejs/prescript.js b/internal/core/runner/nodejs/prescript.js index 85a1b53..d7e9e8e 100644 --- a/internal/core/runner/nodejs/prescript.js +++ b/internal/core/runner/nodejs/prescript.js @@ -3,14 +3,30 @@ const fs = require('fs') const koffi = require('koffi') const lib = koffi.load('./var/sandbox/sandbox-nodejs/nodejs.so') -const difySeccomp = lib.func('void DifySeccomp(int, int, bool)') +const difySeccomp = lib.func('int DifySeccomp(int, int, bool)') const uid = parseInt(argv[2]) const gid = parseInt(argv[3]) const options = JSON.parse(argv[4]) -difySeccomp(uid, gid, options['enable_network']) +const ret = difySeccomp(uid, gid, options['enable_network']) + +if (ret !== 0) { + const errorMessages = { + 1: "Chroot failed", + 2: "Chdir failed", + 3: "Set no new privs failed", + 4: "Seccomp failed", + 5: "Setuid failed", + 6: "Setgid failed", + 7: "Setgroups failed", + 99: "Unknown error", + } + const errorMsg = errorMessages[ret] || `Unknown error code: ${ret}` + console.error(`DifySeccomp failed: ${errorMsg}`) + process.exit(-1) +} const code = fs.readFileSync(3, 'utf8') eval(code) diff --git a/internal/core/runner/python/prescript.py b/internal/core/runner/python/prescript.py index 55dbfcb..142de02 100644 --- a/internal/core/runner/python/prescript.py +++ b/internal/core/runner/python/prescript.py @@ -15,7 +15,7 @@ def excepthook(type, value, tb): lib = ctypes.CDLL("./python.so") lib.DifySeccomp.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool] -lib.DifySeccomp.restype = None +lib.DifySeccomp.restype = ctypes.c_int # get running path running_path = sys.argv[1] @@ -26,7 +26,23 @@ def excepthook(type, value, tb): {{preload}} -lib.DifySeccomp({{uid}}, {{gid}}, {{enable_network}}) + +ret = lib.DifySeccomp({{uid}}, {{gid}}, {{enable_network}}) +if ret != 0: + error_messages = { + 1: "Chroot failed", + 2: "Chdir failed", + 3: "Set no new privs failed", + 4: "Seccomp failed", + 5: "Setuid failed", + 6: "Setgid failed", + 7: "Setgroups failed", + 99: "Unknown error", + } + error_msg = error_messages.get(ret, f"Unknown error code: {ret}") + sys.stderr.write(f"DifySeccomp failed: {error_msg}\n") + sys.stderr.flush() + sys.exit(-1) with os.fdopen(3, "rb") as code_fd: code = code_fd.read().decode("utf-8") diff --git a/internal/core/runner/python/python.h b/internal/core/runner/python/python.h index 85cb3e9..2546364 100644 --- a/internal/core/runner/python/python.h +++ b/internal/core/runner/python/python.h @@ -21,6 +21,11 @@ extern const char *_GoStringPtr(_GoString_ s); /* Start of preamble from import "C" comments. */ +#line 11 "main.go" + +#include + +#line 1 "cgo-generated-wrapper" /* End of preamble from import "C" comments. */ @@ -82,7 +87,7 @@ typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; extern "C" { #endif -extern void DifySeccomp(GoInt uid, GoInt gid, GoUint8 enable_network); +extern int DifySeccomp(GoInt uid, GoInt gid, GoUint8 enable_network); #ifdef __cplusplus } diff --git a/internal/core/runner/uidpool/uid_pool.go b/internal/core/runner/uidpool/uid_pool.go index 08d0437..a629af4 100644 --- a/internal/core/runner/uidpool/uid_pool.go +++ b/internal/core/runner/uidpool/uid_pool.go @@ -1,11 +1,13 @@ package uidpool import ( + "bufio" "context" "errors" "fmt" "log/slog" "os" + "strings" "sync" ) @@ -62,19 +64,62 @@ func AcquireUID(ctx context.Context) (int, error) { return globalPool.Acquire(ctx) } -// ensurePasswdEntries appends sandbox UIDs to /etc/passwd so that -// Python's cleanup (e.g. getpwuid) doesn't trigger blocked syscalls. +// ensurePasswdEntries validates that sandbox UID entries exist in /etc/passwd +// Performs exact matching on the complete entry format func ensurePasswdEntries(min, max int) { - f, err := os.OpenFile("/etc/passwd", os.O_APPEND|os.O_WRONLY, 0644) + // Open /etc/passwd in read-only mode + f, err := os.Open("/etc/passwd") if err != nil { - slog.Warn("failed to open /etc/passwd for UID entries", "err", err) + slog.Error("failed to open /etc/passwd", "err", err) return } defer f.Close() + + // Read and parse file line by line + scanner := bufio.NewScanner(f) + + // Build a set of expected exact strings + expected := make(map[string]bool) for i := min; i < max; i++ { - fmt.Fprintf(f, "sandbox%d:x:%d:0::/nonexistent:/usr/sbin/nologin\n", i, i) + expected[fmt.Sprintf("sandbox%d:x:%d:0::/nonexistent:/usr/sbin/nologin", i, i)] = true + } + + // Track found entries + found := make(map[string]bool) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if expected[line] { + found[line] = true + } + } + + if err := scanner.Err(); err != nil { + slog.Error("error reading /etc/passwd", "err", err) + return + } + + // Find missing entries + var missingUIDs []int + for i := min; i < max; i++ { + expectedLine := fmt.Sprintf("sandbox%d:x:%d:0::/nonexistent:/usr/sbin/nologin", i, i) + if !found[expectedLine] { + missingUIDs = append(missingUIDs, i) + } + } + + // Report errors + if len(missingUIDs) > 0 { + slog.Error("sandbox UID entries missing or incorrect in /etc/passwd", + "missing_uids", missingUIDs, + "count", len(missingUIDs), + "expected_format", "sandbox${UID}:x:${UID}:0::/nonexistent:/usr/sbin/nologin", + "example", fmt.Sprintf("sandbox%d:x:%d:0::/nonexistent:/usr/sbin/nologin", min, min)) + } else { + slog.Info("sandbox UID entries verified", + "range", fmt.Sprintf("%d-%d", min, max-1), + "count", max-min) } - slog.Info("sandbox UID passwd entries created", "range", fmt.Sprintf("%d-%d", min, max-1)) } func ReleaseUID(uid int) { diff --git a/sandbox_userctl.sh b/sandbox_userctl.sh new file mode 100755 index 0000000..2514f5f --- /dev/null +++ b/sandbox_userctl.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Setup sandbox users - Single or Batch mode +set -eu + +if [ "$(id -u)" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +# Single user setup +setup_single() { + local user="sandbox" uid=65537 + if ! id "$user" &>/dev/null; then + useradd -u "$uid" -d /nonexistent -s /usr/sbin/nologin "$user" + echo "Created $user (UID: $uid, GID: $(id -g "$user"))" + else + echo "User $user already exists" + fi +} + +# Batch users setup +setup_batch() { + local min=${1:-10000} max=${2:-11000} + local backup="/etc/passwd.backup.$(date +%Y%m%d_%H%M%S)" + + cp /etc/passwd "$backup" + + # Remove existing sandbox entries + sed -i '/^sandbox[0-9]\+:/d' /etc/passwd + + for i in $(seq $min $((max-1))); do + echo "sandbox${i}:x:${i}:0::/nonexistent:/usr/sbin/nologin" >> /etc/passwd + done + + echo "Created $((max-min)) entries ($min-$((max-1)))" +} + + +echo "=== Setting up single sandbox user ===" +setup_single + +echo -e "\n=== Setting up batch sandbox users ===" +setup_batch + +echo -e "\n=== Setup complete ===" \ No newline at end of file