From 75b9323becd083d8d436b7b7c2f650b6b9ddf6f2 Mon Sep 17 00:00:00 2001 From: gouzi <530971494@qq.com> Date: Sat, 2 Aug 2025 20:57:46 +0800 Subject: [PATCH 1/3] feat: add uv runner --- build/build_amd64.sh | 4 + build/build_arm64.sh | 4 + cmd/dependencies/init.go | 8 + cmd/lib/uv/main.go | 13 ++ conf/config.yaml | 1 + docker/generate.sh | 2 + docker/templates/production.dockerfile | 2 + docker/templates/test.dockerfile | 7 + docker/versions.yaml | 1 + internal/controller/run.go | 14 +- internal/core/lib/uv/add_seccomp.go | 66 +++++++ internal/core/runner/types/runner_options.go | 3 +- internal/core/runner/uv/.gitignore | 1 + internal/core/runner/uv/env.go | 51 +++++ internal/core/runner/uv/env.sh | 61 ++++++ internal/core/runner/uv/prescript.py | 52 +++++ internal/core/runner/uv/setup.go | 63 ++++++ internal/core/runner/uv/uv.go | 192 +++++++++++++++++++ internal/core/runner/uv/uv.h | 81 ++++++++ internal/service/uv.go | 51 +++++ internal/static/config.go | 26 +++ internal/static/config_default_amd64.go | 13 ++ internal/static/config_default_arm64.go | 14 ++ internal/static/uv_syscall/syscalls_amd64.go | 59 ++++++ internal/static/uv_syscall/syscalls_arm64.go | 56 ++++++ internal/types/config.go | 6 +- tests/integration_tests/conf/config.yaml | 1 + tests/integration_tests/init.go | 22 +++ tests/integration_tests/uv_feature_test.go | 143 ++++++++++++++ tests/integration_tests/uv_longchars_test.go | 62 ++++++ tests/integration_tests/uv_malicious_test.go | 77 ++++++++ 31 files changed, 1150 insertions(+), 6 deletions(-) create mode 100644 cmd/lib/uv/main.go create mode 100644 internal/core/lib/uv/add_seccomp.go create mode 100644 internal/core/runner/uv/.gitignore create mode 100644 internal/core/runner/uv/env.go create mode 100644 internal/core/runner/uv/env.sh create mode 100644 internal/core/runner/uv/prescript.py create mode 100644 internal/core/runner/uv/setup.go create mode 100644 internal/core/runner/uv/uv.go create mode 100644 internal/core/runner/uv/uv.h create mode 100644 internal/service/uv.go create mode 100644 internal/static/uv_syscall/syscalls_amd64.go create mode 100644 internal/static/uv_syscall/syscalls_arm64.go create mode 100644 tests/integration_tests/uv_feature_test.go create mode 100644 tests/integration_tests/uv_longchars_test.go create mode 100644 tests/integration_tests/uv_malicious_test.go diff --git a/build/build_amd64.sh b/build/build_amd64.sh index f32fb156..de6f69e1 100755 --- a/build/build_amd64.sh +++ b/build/build_amd64.sh @@ -1,11 +1,15 @@ rm -f internal/core/runner/python/python.so rm -f internal/core/runner/nodejs/nodejs.so +rm -f internal/core/runner/uv/uv.so rm -f /tmp/sandbox-python/python.so rm -f /tmp/sandbox-nodejs/nodejs.so +rm -f /tmp/sandbox-uv/uv.so echo "Building Python lib" CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o internal/core/runner/python/python.so -buildmode=c-shared -ldflags="-s -w" cmd/lib/python/main.go && echo "Building Nodejs lib" && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o internal/core/runner/nodejs/nodejs.so -buildmode=c-shared -ldflags="-s -w" cmd/lib/nodejs/main.go && +echo "Building uv lib" && +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o internal/core/runner/uv/uv.so -buildmode=c-shared -ldflags="-s -w" cmd/lib/uv/main.go && echo "Building main" && GOOS=linux GOARCH=amd64 go build -o main -ldflags="-s -w" cmd/server/main.go echo "Building env" diff --git a/build/build_arm64.sh b/build/build_arm64.sh index 0f75ef60..493ef879 100755 --- a/build/build_arm64.sh +++ b/build/build_arm64.sh @@ -1,11 +1,15 @@ rm -f internal/core/runner/python/python.so rm -f internal/core/runner/nodejs/nodejs.so +rm -f internal/core/runner/uv/uv.so rm -f /tmp/sandbox-python/python.so rm -f /tmp/sandbox-nodejs/nodejs.so +rm -f /tmp/sandbox-uv/uv.so echo "Building Python lib" CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -o internal/core/runner/python/python.so -buildmode=c-shared -ldflags="-s -w" cmd/lib/python/main.go && echo "Building Nodejs lib" && CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -o internal/core/runner/nodejs/nodejs.so -buildmode=c-shared -ldflags="-s -w" cmd/lib/nodejs/main.go && +echo "Building uv lib" && +CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -o internal/core/runner/uv/uv.so -buildmode=c-shared -ldflags="-s -w" cmd/lib/uv/main.go && echo "Building main" && GOOS=linux GOARCH=arm64 go build -o main -ldflags="-s -w" cmd/server/main.go echo "Building env" diff --git a/cmd/dependencies/init.go b/cmd/dependencies/init.go index 76746000..008ff9e2 100644 --- a/cmd/dependencies/init.go +++ b/cmd/dependencies/init.go @@ -2,6 +2,7 @@ package main import ( "github.com/langgenius/dify-sandbox/internal/core/runner/python" + "github.com/langgenius/dify-sandbox/internal/core/runner/uv" "github.com/langgenius/dify-sandbox/internal/static" "github.com/langgenius/dify-sandbox/internal/utils/log" ) @@ -15,4 +16,11 @@ func main() { } log.Info("Python dependencies initialized successfully") + + err = uv.PrepareUvDependenciesEnv() + if err != nil { + log.Panic("failed to initialize uv dependencies sandbox: %v", err) + } + + log.Info("UV dependencies initialized successfully") } diff --git a/cmd/lib/uv/main.go b/cmd/lib/uv/main.go new file mode 100644 index 00000000..2a776870 --- /dev/null +++ b/cmd/lib/uv/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "github.com/langgenius/dify-sandbox/internal/core/lib/uv" +) +import "C" + +//export DifySeccomp +func DifySeccomp(uid int, gid int, enable_network bool) { + uv.InitSeccomp(uid, gid, enable_network) +} + +func main() {} diff --git a/conf/config.yaml b/conf/config.yaml index 1f53cdef..da25a2e4 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -6,6 +6,7 @@ max_workers: 4 max_requests: 50 worker_timeout: 5 python_path: /usr/local/bin/python3 +uv_path: /usr/local/bin/uv enable_network: True # please make sure there is no network risk in your environment enable_preload: False # please keep it as False for security purposes allowed_syscalls: # please leave it empty if you have no idea how seccomp works diff --git a/docker/generate.sh b/docker/generate.sh index 4ce8fbfd..0d610482 100755 --- a/docker/generate.sh +++ b/docker/generate.sh @@ -44,6 +44,7 @@ echo "Reading version configuration..." PYTHON_VERSION=$(yq eval '.versions.python' "$VERSIONS_FILE") GOLANG_VERSION=$(yq eval '.versions.golang' "$VERSIONS_FILE") NODEJS_VERSION=$(yq eval '.versions.nodejs' "$VERSIONS_FILE") +UV_VERSION=$(yq eval '.versions.uv' "$VERSIONS_FILE") PYTHON_PACKAGES=$(yq eval '.versions.python_packages' "$VERSIONS_FILE") DEBIAN_MIRROR=$(yq eval '.mirrors.debian' "$VERSIONS_FILE") NODEJS_MIRROR=$(yq eval '.mirrors.nodejs' "$VERSIONS_FILE") @@ -79,6 +80,7 @@ esac sed -e "s/\${PYTHON_VERSION}/${PYTHON_VERSION}/g" \ -e "s/\${GOLANG_VERSION}/${GOLANG_VERSION}/g" \ -e "s/\${NODEJS_VERSION}/${NODEJS_VERSION}/g" \ + -e "s|\${UV_VERSION}|${UV_VERSION}|g" \ -e "s|\${PYTHON_PACKAGES}|${PYTHON_PACKAGES}|g" \ -e "s|\${DEBIAN_MIRROR}|${DEBIAN_MIRROR}|g" \ -e "s|\${NODEJS_MIRROR}|${NODEJS_MIRROR}|g" \ diff --git a/docker/templates/production.dockerfile b/docker/templates/production.dockerfile index fa01274d..8f7dcca1 100644 --- a/docker/templates/production.dockerfile +++ b/docker/templates/production.dockerfile @@ -4,6 +4,7 @@ ARG DEBIAN_MIRROR="http://deb.debian.org/debian testing main" ARG PYTHON_PACKAGES="httpx==0.27.2 requests==2.32.3 jinja2==3.1.6 PySocks httpx[socks]" ARG NODEJS_VERSION=v20.11.1 ARG NODEJS_MIRROR="https://npmmirror.com/mirrors/node" +ARG UV_VERSION=0.8.3 ARG TARGETARCH FROM python:${PYTHON_VERSION} @@ -27,6 +28,7 @@ RUN echo "deb ${DEBIAN_MIRROR}" > /etc/apt/sources.list \ # Copy binary files COPY main /main COPY env /env +COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /uvx /usr/local/bin/ # Copy configuration files COPY conf/config.yaml /conf/config.yaml diff --git a/docker/templates/test.dockerfile b/docker/templates/test.dockerfile index df16e020..df90e8d4 100644 --- a/docker/templates/test.dockerfile +++ b/docker/templates/test.dockerfile @@ -6,6 +6,7 @@ ARG PYTHON_PACKAGES="httpx==0.27.2 requests==2.32.3 jinja2==3.1.6 PySocks httpx[ ARG NODEJS_VERSION=v20.11.1 ARG NODEJS_MIRROR="https://npmmirror.com/mirrors/node" ARG GOLANG_MIRROR="https://golang.org/dl" +ARG UV_VERSION=0.8.3 ARG TARGETARCH # Build stage @@ -55,9 +56,13 @@ WORKDIR /app # Copy source code COPY . /app +# Copy uv binary files +COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /uvx /usr/local/bin/ + # Copy binary files from build stage COPY --from=builder /app/internal/core/runner/python/python.so /app/internal/core/runner/python/python.so COPY --from=builder /app/internal/core/runner/nodejs/nodejs.so /app/internal/core/runner/nodejs/nodejs.so +COPY --from=builder /app/internal/core/runner/uv/uv.so /app/internal/core/runner/uv/uv.so # Copy configuration files COPY conf/config.yaml /conf/config.yaml @@ -66,6 +71,8 @@ COPY dependencies/python-requirements.txt /dependencies/python-requirements.txt # Install Python dependencies RUN pip3 install --no-cache-dir ${PYTHON_PACKAGES} +RUN uv python install 3.10.18 + # Install Node.js based on architecture RUN case "${TARGETARCH}" in \ "amd64") \ diff --git a/docker/versions.yaml b/docker/versions.yaml index 829c3e69..5d7ea87b 100644 --- a/docker/versions.yaml +++ b/docker/versions.yaml @@ -4,6 +4,7 @@ versions: python: "3.10-slim-bookworm" golang: "1.23.9" nodejs: "v20.11.1" + uv: "0.8.3" # Python packages (unified configuration) python_packages: "httpx==0.27.2 requests==2.32.3 jinja2==3.1.6 PySocks httpx[socks]" diff --git a/internal/controller/run.go b/internal/controller/run.go index 42087569..57e82fd5 100644 --- a/internal/controller/run.go +++ b/internal/controller/run.go @@ -9,10 +9,11 @@ import ( func RunSandboxController(c *gin.Context) { BindRequest(c, func(req struct { - Language string `json:"language" form:"language" binding:"required"` - Code string `json:"code" form:"code" binding:"required"` - Preload string `json:"preload" form:"preload"` - EnableNetwork bool `json:"enable_network" form:"enable_network"` + Language string `json:"language" form:"language" binding:"required"` + Code string `json:"code" form:"code" binding:"required"` + Preload string `json:"preload" form:"preload"` + EnableNetwork bool `json:"enable_network" form:"enable_network"` + Dependenchies []runner_types.Dependency `json:"dependencies" form:"dependencies"` }) { switch req.Language { case "python3": @@ -23,6 +24,11 @@ func RunSandboxController(c *gin.Context) { c.JSON(200, service.RunNodeJsCode(req.Code, req.Preload, &runner_types.RunnerOptions{ EnableNetwork: req.EnableNetwork, })) + case "uv": + c.JSON(200, service.RunUvCode(req.Code, req.Preload, &runner_types.RunnerOptions{ + EnableNetwork: req.EnableNetwork, + Dependenchies: req.Dependenchies, + })) default: c.JSON(400, types.ErrorResponse(-400, "unsupported language")) } diff --git a/internal/core/lib/uv/add_seccomp.go b/internal/core/lib/uv/add_seccomp.go new file mode 100644 index 00000000..0a5948ad --- /dev/null +++ b/internal/core/lib/uv/add_seccomp.go @@ -0,0 +1,66 @@ +package uv + +import ( + "os" + "strconv" + "strings" + "syscall" + + "github.com/langgenius/dify-sandbox/internal/core/lib" + "github.com/langgenius/dify-sandbox/internal/static/uv_syscall" +) + +//var allow_syscalls = []int{} + +func InitSeccomp(uid int, gid int, enable_network bool) error { + err := syscall.Chroot(".") + if err != nil { + return err + } + err = syscall.Chdir("/") + if err != nil { + return err + } + + lib.SetNoNewPrivs() + + allowed_syscalls := []int{} + allowed_not_kill_syscalls := []int{} + allowed_not_kill_syscalls = append(allowed_not_kill_syscalls, uv_syscall.ALLOW_ERROR_SYSCALLS...) + + allowed_syscall := os.Getenv("ALLOWED_SYSCALLS") + if allowed_syscall != "" { + nums := strings.Split(allowed_syscall, ",") + for num := range nums { + syscall, err := strconv.Atoi(nums[num]) + if err != nil { + continue + } + allowed_syscalls = append(allowed_syscalls, syscall) + } + } else { + allowed_syscalls = append(allowed_syscalls, uv_syscall.ALLOW_SYSCALLS...) + if enable_network { + allowed_syscalls = append(allowed_syscalls, uv_syscall.ALLOW_NETWORK_SYSCALLS...) + } + } + + err = lib.Seccomp(allowed_syscalls, allowed_not_kill_syscalls) + if err != nil { + return err + } + + // setuid + err = syscall.Setuid(uid) + if err != nil { + return err + } + + // setgid + err = syscall.Setgid(gid) + if err != nil { + return err + } + + return nil +} diff --git a/internal/core/runner/types/runner_options.go b/internal/core/runner/types/runner_options.go index 204e4e17..8b8ecd15 100644 --- a/internal/core/runner/types/runner_options.go +++ b/internal/core/runner/types/runner_options.go @@ -8,7 +8,8 @@ type Dependency struct { } type RunnerOptions struct { - EnableNetwork bool `json:"enable_network"` + EnableNetwork bool `json:"enable_network"` + Dependenchies []Dependency `json:"dependencies"` } func (r *RunnerOptions) Json() string { diff --git a/internal/core/runner/uv/.gitignore b/internal/core/runner/uv/.gitignore new file mode 100644 index 00000000..c5a21042 --- /dev/null +++ b/internal/core/runner/uv/.gitignore @@ -0,0 +1 @@ +uv.so \ No newline at end of file diff --git a/internal/core/runner/uv/env.go b/internal/core/runner/uv/env.go new file mode 100644 index 00000000..0b25e052 --- /dev/null +++ b/internal/core/runner/uv/env.go @@ -0,0 +1,51 @@ +package uv + +import ( + _ "embed" + "os" + "os/exec" + "path" + + "github.com/langgenius/dify-sandbox/internal/core/runner" + "github.com/langgenius/dify-sandbox/internal/static" + "github.com/langgenius/dify-sandbox/internal/utils/log" +) + +//go:embed env.sh +var env_script string + +func PrepareUvDependenciesEnv() error { + config := static.GetDifySandboxGlobalConfigurations() + + runner := runner.TempDirRunner{} + err := runner.WithTempDir("/", []string{}, func(root_path string) error { + err := os.WriteFile(path.Join(root_path, "env.sh"), []byte(env_script), 0755) + if err != nil { + return err + } + + for _, lib_path := range config.UvLibPaths { + // check if the lib path is available + if _, err := os.Stat(lib_path); err != nil { + log.Warn("uv lib path %s is not available", lib_path) + continue + } + exec_cmd := exec.Command( + "bash", + path.Join(root_path, "env.sh"), + lib_path, + LIB_PATH, + ) + exec_cmd.Stderr = os.Stderr + + if err := exec_cmd.Run(); err != nil { + return err + } + } + + os.RemoveAll(root_path) + return nil + }) + + return err +} diff --git a/internal/core/runner/uv/env.sh b/internal/core/runner/uv/env.sh new file mode 100644 index 00000000..9a87dc45 --- /dev/null +++ b/internal/core/runner/uv/env.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Check if the correct number of arguments are provided +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +src="$1" +dest="$2" + +# Function to copy and link files +copy_and_link() { + local src_file="$1" + local dest_file="$2" + + if [ -L "$src_file" ]; then + # If src_file is a symbolic link, copy it without changing permissions + cp -P "$src_file" "$dest_file" + elif [ -b "$src_file" ] || [ -c "$src_file" ]; then + # If src_file is a device file, copy it and change permissions + cp "$src_file" "$dest_file" + chmod 444 "$dest_file" + else + # Otherwise, create a hard link and change the permissions to read-only + ln -f "$src_file" "$dest_file" 2>/dev/null || { cp "$src_file" "$dest_file" && chmod 444 "$dest_file"; } + fi +} + +# Check if src is a file or directory +if [ -f "$src" ]; then + # src is a file, create hard link directly in dest + mkdir -p "$(dirname "$dest/$src")" + copy_and_link "$src" "$dest/$src" +elif [ -d "$src" ]; then + # src is a directory, process as before + mkdir -p "$dest/$src" + + # Find all files in the source directory + find "$src" -type f,l | while read -r file; do + # Get the relative path of the file + rel_path="${file#$src/}" + # Get the directory of the relative path + rel_dir=$(dirname "$rel_path") + # Create the same directory structure in the destination + mkdir -p "$dest/$src/$rel_dir" + # Copy and link the file + copy_and_link "$file" "$dest/$src/$rel_path" + done +else + echo "Error: $src is neither a file nor a directory" + exit 1 +fi + +# copy dev/urandom if it exists +if [ ! -d /var/sandbox/sandbox-uv/dev ]; then + mkdir -p /var/sandbox/sandbox-uv/dev +fi +if [ ! -e /var/sandbox/sandbox-uv/dev/urandom ]; then + cp -a /dev/urandom /var/sandbox/sandbox-uv/dev/ +fi diff --git a/internal/core/runner/uv/prescript.py b/internal/core/runner/uv/prescript.py new file mode 100644 index 00000000..1a73ff71 --- /dev/null +++ b/internal/core/runner/uv/prescript.py @@ -0,0 +1,52 @@ +# /// script +# requires-python = "==3.10.18" +# dependencies = [{{dependencies}}] +# /// + +import ctypes +import os +import sys +import traceback +# setup sys.excepthook +def excepthook(type, value, tb): + sys.stderr.write("".join(traceback.format_exception(type, value, tb))) + sys.stderr.flush() + sys.exit(-1) + +sys.excepthook = excepthook + +lib = ctypes.CDLL("./uv.so") +lib.DifySeccomp.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool] +lib.DifySeccomp.restype = None + +# get running path +running_path = sys.argv[1] +if not running_path: + exit(-1) + +# get decrypt key +key = sys.argv[2] +if not key: + exit(-1) + +from base64 import b64decode +key = b64decode(key) + +os.chdir(running_path) + +{{preload}} + +lib.DifySeccomp({{uid}}, {{gid}}, {{enable_network}}) + +code = b64decode("{{code}}") + +def decrypt(code, key): + key_len = len(key) + code_len = len(code) + code = bytearray(code) + for i in range(code_len): + code[i] = code[i] ^ key[i % key_len] + return bytes(code) + +code = decrypt(code, key) +exec(code) \ No newline at end of file diff --git a/internal/core/runner/uv/setup.go b/internal/core/runner/uv/setup.go new file mode 100644 index 00000000..8e3460f5 --- /dev/null +++ b/internal/core/runner/uv/setup.go @@ -0,0 +1,63 @@ +package uv + +import ( + _ "embed" + "fmt" + "os" + "path" + + "github.com/langgenius/dify-sandbox/internal/utils/log" +) + +//go:embed uv.so +var uv_lib []byte + +const ( + LIB_PATH = "/var/sandbox/sandbox-uv" + LIB_NAME = "uv.so" +) + +func init() { + releaseLibBinary(true) +} + +func releaseLibBinary(force_remove_old_lib bool) { + log.Info("initializing uv runner environment...") + // remove the old lib + if _, err := os.Stat(path.Join(LIB_PATH, LIB_NAME)); err == nil { + if force_remove_old_lib { + err := os.Remove(path.Join(LIB_PATH, LIB_NAME)) + if err != nil { + log.Panic(fmt.Sprintf("failed to remove %s", path.Join(LIB_PATH, LIB_NAME))) + } + + // write the new lib + err = os.MkdirAll(LIB_PATH, 0755) + if err != nil { + log.Panic(fmt.Sprintf("failed to create %s", LIB_PATH)) + } + err = os.WriteFile(path.Join(LIB_PATH, LIB_NAME), uv_lib, 0755) + if err != nil { + log.Panic(fmt.Sprintf("failed to write %s", path.Join(LIB_PATH, LIB_NAME))) + } + } + } else { + err = os.MkdirAll(LIB_PATH, 0755) + if err != nil { + log.Panic(fmt.Sprintf("failed to create %s", LIB_PATH)) + } + err = os.WriteFile(path.Join(LIB_PATH, LIB_NAME), uv_lib, 0755) + if err != nil { + log.Panic(fmt.Sprintf("failed to write %s", path.Join(LIB_PATH, LIB_NAME))) + } + log.Info("uv runner environment initialized") + } +} + +func checkLibAvaliable() bool { + if _, err := os.Stat(path.Join(LIB_PATH, LIB_NAME)); err != nil { + return false + } + + return true +} diff --git a/internal/core/runner/uv/uv.go b/internal/core/runner/uv/uv.go new file mode 100644 index 00000000..15aa3bf7 --- /dev/null +++ b/internal/core/runner/uv/uv.go @@ -0,0 +1,192 @@ +package uv + +import ( + "crypto/rand" + _ "embed" + "encoding/base64" + "fmt" + "os" + "os/exec" + "path" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/langgenius/dify-sandbox/internal/core/runner" + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/static" +) + +type UvRunner struct { + runner.TempDirRunner +} + +//go:embed prescript.py +var sandbox_fs []byte + +func (p *UvRunner) Run( + code string, + timeout time.Duration, + stdin []byte, + preload string, + options *types.RunnerOptions, +) (chan []byte, chan []byte, chan bool, error) { + configuration := static.GetDifySandboxGlobalConfigurations() + + // initialize the environment + untrusted_code_path, key, err := p.InitializeEnvironment(code, preload, options) + if err != nil { + return nil, nil, nil, err + } + + // capture the output + output_handler := runner.NewOutputCaptureRunner() + output_handler.SetTimeout(timeout) + output_handler.SetAfterExitHook(func() { + // remove untrusted code + os.Remove(untrusted_code_path) + }) + + cmd := exec.Command( + configuration.UvPath, + "run", + "--quiet", + "--script", + untrusted_code_path, + LIB_PATH, + key, + ) + cmd.Env = []string{"UV_CACHE_DIR=" + LIB_PATH + "/root/.cache/uv"} + cmd.Dir = LIB_PATH + + if configuration.Proxy.Socks5 != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("HTTPS_PROXY=%s", configuration.Proxy.Socks5)) + cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_PROXY=%s", configuration.Proxy.Socks5)) + } else if configuration.Proxy.Https != "" || configuration.Proxy.Http != "" { + if configuration.Proxy.Https != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("HTTPS_PROXY=%s", configuration.Proxy.Https)) + } + if configuration.Proxy.Http != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_PROXY=%s", configuration.Proxy.Http)) + } + } else if configuration.UvMirrorURL != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("UV_INDEX_URL=%s", configuration.UvMirrorURL)) + } + + if len(configuration.AllowedSyscalls) > 0 { + cmd.Env = append(cmd.Env, + fmt.Sprintf("ALLOWED_SYSCALLS=%s", + strings.Trim(strings.Join(strings.Fields(fmt.Sprint(configuration.AllowedSyscalls)), ","), "[]"), + ), + ) + } + + err = output_handler.CaptureOutput(cmd) + if err != nil { + return nil, nil, nil, err + } + + return output_handler.GetStdout(), output_handler.GetStderr(), output_handler.GetDone(), nil +} + +func (p *UvRunner) InitializeEnvironment(code string, preload string, options *types.RunnerOptions) (string, string, error) { + if !checkLibAvaliable() { + // ensure environment is reversed + releaseLibBinary(false) + } + + // create a tmp dir and copy the python script + temp_code_name := strings.ReplaceAll(uuid.New().String(), "-", "_") + temp_code_name = strings.ReplaceAll(temp_code_name, "/", ".") + + script := strings.Replace( + string(sandbox_fs), + "{{uid}}", strconv.Itoa(static.SANDBOX_USER_UID), 1, + ) + + script = strings.Replace( + script, + "{{gid}}", strconv.Itoa(static.SANDBOX_GROUP_ID), 1, + ) + + if options.EnableNetwork { + script = strings.Replace( + script, + "{{enable_network}}", "1", 1, + ) + } else { + script = strings.Replace( + script, + "{{enable_network}}", "0", 1, + ) + } + + script = strings.Replace( + script, + "{{preload}}", + fmt.Sprintf("%s\n", preload), + 1, + ) + + dependencies := []string{} + if options != nil && options.Dependenchies != nil { + for _, dep := range options.Dependenchies { + if dep.Version == "" { + dependencies = append(dependencies, fmt.Sprintf("\"%s\"", dep.Name)) + } else { + dependencies = append(dependencies, fmt.Sprintf("\"%s==%s\"", dep.Name, dep.Version)) + } + } + } + + // if there are dependencies, add the package path to the script + if len(dependencies) != 0 { + code = fmt.Sprintf("import sys;import sysconfig;sys.path.insert(0, sysconfig.get_paths()[\"purelib\"].replace(\"%s\", \"\"))\n%s", LIB_PATH, code) + } + + script = strings.Replace( + script, + "{{dependencies}}", + strings.Join(dependencies, ", "), + 1, + ) + + // generate a random 512 bit key + key_len := 64 + key := make([]byte, key_len) + _, err := rand.Read(key) + if err != nil { + return "", "", err + } + + // encrypt the code + encrypted_code := make([]byte, len(code)) + for i := 0; i < len(code); i++ { + encrypted_code[i] = code[i] ^ key[i%key_len] + } + + // encode code using base64 + code = base64.StdEncoding.EncodeToString(encrypted_code) + // encode key using base64 + encoded_key := base64.StdEncoding.EncodeToString(key) + + code = strings.Replace( + script, + "{{code}}", + code, + 1, + ) + + untrusted_code_path := fmt.Sprintf("%s/tmp/%s.py", LIB_PATH, temp_code_name) + err = os.MkdirAll(path.Dir(untrusted_code_path), 0755) + if err != nil { + return "", "", err + } + err = os.WriteFile(untrusted_code_path, []byte(code), 0755) + if err != nil { + return "", "", err + } + + return untrusted_code_path, encoded_key, nil +} diff --git a/internal/core/runner/uv/uv.h b/internal/core/runner/uv/uv.h new file mode 100644 index 00000000..c1a17b3c --- /dev/null +++ b/internal/core/runner/uv/uv.h @@ -0,0 +1,81 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package command-line-arguments */ + + +#line 1 "cgo-builtin-export-prolog" + +#include + +#ifndef GO_CGO_EXPORT_PROLOGUE_H +#define GO_CGO_EXPORT_PROLOGUE_H + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef struct { const char *p; ptrdiff_t n; } _GoString_; +#endif + +#endif + +/* Start of preamble from import "C" comments. */ + + + + +/* End of preamble from import "C" comments. */ + + +/* Start of boilerplate cgo prologue. */ +#line 1 "cgo-gcc-export-header-prolog" + +#ifndef GO_CGO_PROLOGUE_H +#define GO_CGO_PROLOGUE_H + +typedef signed char GoInt8; +typedef unsigned char GoUint8; +typedef short GoInt16; +typedef unsigned short GoUint16; +typedef int GoInt32; +typedef unsigned int GoUint32; +typedef long long GoInt64; +typedef unsigned long long GoUint64; +typedef GoInt64 GoInt; +typedef GoUint64 GoUint; +typedef size_t GoUintptr; +typedef float GoFloat32; +typedef double GoFloat64; +#ifdef _MSC_VER +#include +typedef _Fcomplex GoComplex64; +typedef _Dcomplex GoComplex128; +#else +typedef float _Complex GoComplex64; +typedef double _Complex GoComplex128; +#endif + +/* + static assertion to make sure the file is being used on architecture + at least with matching size of GoInt. +*/ +typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef _GoString_ GoString; +#endif +typedef void *GoMap; +typedef void *GoChan; +typedef struct { void *t; void *v; } GoInterface; +typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; + +#endif + +/* End of boilerplate cgo prologue. */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern void DifySeccomp(GoInt uid, GoInt gid, GoUint8 enable_network); + +#ifdef __cplusplus +} +#endif diff --git a/internal/service/uv.go b/internal/service/uv.go new file mode 100644 index 00000000..af7ead65 --- /dev/null +++ b/internal/service/uv.go @@ -0,0 +1,51 @@ +package service + +import ( + "time" + + runner_types "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/core/runner/uv" + "github.com/langgenius/dify-sandbox/internal/static" + "github.com/langgenius/dify-sandbox/internal/types" +) + +func RunUvCode(code string, preload string, options *runner_types.RunnerOptions) *types.DifySandboxResponse { + if err := checkOptions(options); err != nil { + return types.ErrorResponse(-400, err.Error()) + } + + if !static.GetDifySandboxGlobalConfigurations().EnablePreload { + preload = "" + } + + timeout := time.Duration( + static.GetDifySandboxGlobalConfigurations().WorkerTimeout * int(time.Second), + ) + + runner := uv.UvRunner{} + stdout, stderr, done, err := runner.Run(code, timeout, nil, preload, options) + if err != nil { + return types.ErrorResponse(-500, err.Error()) + } + + stdout_str := "" + stderr_str := "" + + defer close(done) + defer close(stdout) + defer close(stderr) + + for { + select { + case <-done: + return types.SuccessResponse(&RunCodeResponse{ + Stdout: stdout_str, + Stderr: stderr_str, + }) + case out := <-stdout: + stdout_str += string(out) + case err := <-stderr: + stderr_str += string(err) + } + } +} diff --git a/internal/static/config.go b/internal/static/config.go index 28c443d0..aa566030 100644 --- a/internal/static/config.go +++ b/internal/static/config.go @@ -102,6 +102,32 @@ func InitConfig(path string) error { difySandboxGlobalConfigurations.NodejsPath = "/usr/local/bin/node" } + uv_path := os.Getenv("UV_PATH") + if uv_path != "" { + difySandboxGlobalConfigurations.UvPath = uv_path + } + if difySandboxGlobalConfigurations.UvPath == "" { + difySandboxGlobalConfigurations.UvPath = "/usr/local/bin/uv" + } + uv_lib_path := os.Getenv("UV_LIB_PATH") + if uv_lib_path != "" { + difySandboxGlobalConfigurations.UvLibPaths = strings.Split(uv_lib_path, ",") + } + if len(difySandboxGlobalConfigurations.UvLibPaths) == 0 { + difySandboxGlobalConfigurations.UvLibPaths = DEFAULT_UV_LIB_REQUIREMENTS + } + uv_cache_path := os.Getenv("UV_CACHE_PATH") + if uv_cache_path != "" { + difySandboxGlobalConfigurations.UvCachePath = uv_cache_path + } + if difySandboxGlobalConfigurations.UvCachePath == "" { + difySandboxGlobalConfigurations.UvCachePath = "/usr/.cache/uv" + } + uv_mirror_url := os.Getenv("UV_MIRROR_URL") + if uv_mirror_url != "" { + difySandboxGlobalConfigurations.UvMirrorURL = uv_mirror_url + } + enable_network := os.Getenv("ENABLE_NETWORK") if enable_network != "" { difySandboxGlobalConfigurations.EnableNetwork, _ = strconv.ParseBool(enable_network) diff --git a/internal/static/config_default_amd64.go b/internal/static/config_default_amd64.go index c3ba888c..8b887b1f 100644 --- a/internal/static/config_default_amd64.go +++ b/internal/static/config_default_amd64.go @@ -17,3 +17,16 @@ var DEFAULT_PYTHON_LIB_REQUIREMENTS = []string{ "/usr/share/zoneinfo", "/etc/timezone", } + +var DEFAULT_UV_LIB_REQUIREMENTS = []string{ + "/root/.local/share/uv/python", + "/etc/ssl/certs/ca-certificates.crt", + "/etc/nsswitch.conf", + "/etc/hosts", + "/etc/resolv.conf", + "/run/systemd/resolve/stub-resolv.conf", + "/run/resolvconf/resolv.conf", + "/etc/localtime", + "/usr/share/zoneinfo", + "/etc/timezone", +} diff --git a/internal/static/config_default_arm64.go b/internal/static/config_default_arm64.go index b8df2759..08e71849 100644 --- a/internal/static/config_default_arm64.go +++ b/internal/static/config_default_arm64.go @@ -17,3 +17,17 @@ var DEFAULT_PYTHON_LIB_REQUIREMENTS = []string{ "/usr/share/zoneinfo", "/etc/timezone", } + +var DEFAULT_UV_LIB_REQUIREMENTS = []string{ + "/root/.local/share/uv/python", + "/usr/lib/aarch64-linux-gnu", + "/etc/ssl/certs/ca-certificates.crt", + "/etc/nsswitch.conf", + "/etc/hosts", + "/etc/resolv.conf", + "/run/systemd/resolve/stub-resolv.conf", + "/run/resolvconf/resolv.conf", + "/etc/localtime", + "/usr/share/zoneinfo", + "/etc/timezone", +} diff --git a/internal/static/uv_syscall/syscalls_amd64.go b/internal/static/uv_syscall/syscalls_amd64.go new file mode 100644 index 00000000..c3c7d617 --- /dev/null +++ b/internal/static/uv_syscall/syscalls_amd64.go @@ -0,0 +1,59 @@ +//go:build linux && amd64 + +package uv_syscall + +import "syscall" + +const ( + SYS_GETRANDOM = 318 + SYS_RSEQ = 334 + SYS_SENDMMSG = 307 + SYS_CLONE3 = 435 +) + +var ALLOW_SYSCALLS = []int{ + // file io + syscall.SYS_NEWFSTATAT, syscall.SYS_IOCTL, syscall.SYS_LSEEK, syscall.SYS_GETDENTS64, + syscall.SYS_WRITE, syscall.SYS_CLOSE, syscall.SYS_OPENAT, syscall.SYS_READ, + + // thread + syscall.SYS_FUTEX, syscall.SYS_CAPGET, syscall.SYS_CAPSET, + // memory + syscall.SYS_MMAP, syscall.SYS_BRK, syscall.SYS_MPROTECT, syscall.SYS_MUNMAP, syscall.SYS_RT_SIGRETURN, + syscall.SYS_MREMAP, + + // user/group + syscall.SYS_SETUID, syscall.SYS_SETGID, syscall.SYS_GETUID, + // process + syscall.SYS_GETPID, syscall.SYS_GETPPID, syscall.SYS_GETTID, + syscall.SYS_EXIT, syscall.SYS_EXIT_GROUP, + syscall.SYS_TGKILL, syscall.SYS_RT_SIGACTION, syscall.SYS_IOCTL, + syscall.SYS_SCHED_YIELD, + syscall.SYS_SET_ROBUST_LIST, syscall.SYS_GET_ROBUST_LIST, SYS_RSEQ, + syscall.SYS_EXECVE, syscall.SYS_PRCTL, + + // time + syscall.SYS_CLOCK_GETTIME, syscall.SYS_GETTIMEOFDAY, syscall.SYS_NANOSLEEP, + syscall.SYS_EPOLL_CREATE1, + syscall.SYS_EPOLL_CTL, syscall.SYS_CLOCK_NANOSLEEP, syscall.SYS_PSELECT6, + syscall.SYS_TIME, + + syscall.SYS_RT_SIGPROCMASK, syscall.SYS_SIGALTSTACK, SYS_GETRANDOM, + + // uv + syscall.SYS_ACCESS, syscall.SYS_LSTAT, syscall.SYS_STAT, syscall.SYS_STATFS, syscall.SYS_READLINKAT, syscall.SYS_CHDIR, + syscall.SYS_FCHOWN, +} + +var ALLOW_ERROR_SYSCALLS = []int{ + syscall.SYS_CLONE, + syscall.SYS_MKDIRAT, + syscall.SYS_MKDIR, +} + +var ALLOW_NETWORK_SYSCALLS = []int{ + syscall.SYS_SOCKET, syscall.SYS_CONNECT, syscall.SYS_BIND, syscall.SYS_LISTEN, syscall.SYS_ACCEPT, syscall.SYS_SENDTO, syscall.SYS_RECVFROM, + syscall.SYS_GETSOCKNAME, syscall.SYS_RECVMSG, syscall.SYS_GETPEERNAME, syscall.SYS_SETSOCKOPT, syscall.SYS_PPOLL, syscall.SYS_UNAME, + syscall.SYS_SENDMSG, SYS_SENDMMSG, syscall.SYS_GETSOCKOPT, + syscall.SYS_FSTAT, syscall.SYS_FCNTL, syscall.SYS_FSTATFS, syscall.SYS_POLL, syscall.SYS_EPOLL_PWAIT, +} diff --git a/internal/static/uv_syscall/syscalls_arm64.go b/internal/static/uv_syscall/syscalls_arm64.go new file mode 100644 index 00000000..01f416c1 --- /dev/null +++ b/internal/static/uv_syscall/syscalls_arm64.go @@ -0,0 +1,56 @@ +//go:build linux && arm64 + +package uv_syscall + +import ( + "syscall" +) + +const ( + SYS_RSEQ = 293 +) + +var ALLOW_SYSCALLS = []int{ + // file io + syscall.SYS_WRITE, syscall.SYS_CLOSE, syscall.SYS_OPENAT, syscall.SYS_READ, syscall.SYS_LSEEK, syscall.SYS_GETDENTS64, + + // thread + syscall.SYS_FUTEX, + + // memory + syscall.SYS_MMAP, syscall.SYS_BRK, syscall.SYS_MPROTECT, syscall.SYS_MUNMAP, syscall.SYS_RT_SIGRETURN, syscall.SYS_RT_SIGPROCMASK, + syscall.SYS_SIGALTSTACK, syscall.SYS_MREMAP, + + // user/group + syscall.SYS_SETUID, syscall.SYS_SETGID, syscall.SYS_GETUID, + + // process + syscall.SYS_GETPID, syscall.SYS_GETPPID, syscall.SYS_GETTID, + syscall.SYS_EXIT, syscall.SYS_EXIT_GROUP, + syscall.SYS_TGKILL, syscall.SYS_RT_SIGACTION, + syscall.SYS_IOCTL, syscall.SYS_SCHED_YIELD, + syscall.SYS_GET_ROBUST_LIST, syscall.SYS_SET_ROBUST_LIST, + SYS_RSEQ, + + // time + syscall.SYS_EPOLL_CREATE1, + syscall.SYS_CLOCK_GETTIME, syscall.SYS_GETTIMEOFDAY, syscall.SYS_NANOSLEEP, + syscall.SYS_EPOLL_CTL, syscall.SYS_CLOCK_NANOSLEEP, syscall.SYS_PSELECT6, + syscall.SYS_TIMERFD_CREATE, syscall.SYS_TIMERFD_SETTIME, syscall.SYS_TIMERFD_GETTIME, + + // get random + syscall.SYS_GETRANDOM, +} + +var ALLOW_ERROR_SYSCALLS = []int{ + syscall.SYS_CLONE, + syscall.SYS_MKDIRAT, +} + +var ALLOW_NETWORK_SYSCALLS = []int{ + syscall.SYS_SOCKET, syscall.SYS_CONNECT, syscall.SYS_BIND, syscall.SYS_LISTEN, syscall.SYS_ACCEPT, syscall.SYS_SENDTO, + syscall.SYS_RECVFROM, syscall.SYS_RECVMSG, syscall.SYS_GETSOCKOPT, + syscall.SYS_GETSOCKNAME, syscall.SYS_GETPEERNAME, syscall.SYS_SETSOCKOPT, + syscall.SYS_PPOLL, syscall.SYS_UNAME, syscall.SYS_SENDMMSG, + syscall.SYS_FSTATAT, syscall.SYS_FSTAT, syscall.SYS_FSTATFS, syscall.SYS_EPOLL_PWAIT, +} diff --git a/internal/types/config.go b/internal/types/config.go index 34ac33b8..8bf1f14b 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -14,6 +14,10 @@ type DifySandboxGlobalConfigurations struct { PythonPipMirrorURL string `yaml:"python_pip_mirror_url"` PythonDepsUpdateInterval string `yaml:"python_deps_update_interval"` NodejsPath string `yaml:"nodejs_path"` + UvPath string `yaml:"uv_path"` + UvLibPaths []string `yaml:"uv_lib_path"` + UvCachePath string `yaml:"uv_cache_path"` + UvMirrorURL string `yaml:"uv_mirror_url"` EnableNetwork bool `yaml:"enable_network"` EnablePreload bool `yaml:"enable_preload"` AllowedSyscalls []int `yaml:"allowed_syscalls"` @@ -22,4 +26,4 @@ type DifySandboxGlobalConfigurations struct { Https string `yaml:"https"` Http string `yaml:"http"` } `yaml:"proxy"` -} \ No newline at end of file +} diff --git a/tests/integration_tests/conf/config.yaml b/tests/integration_tests/conf/config.yaml index f30b53d0..85fa3dd3 100644 --- a/tests/integration_tests/conf/config.yaml +++ b/tests/integration_tests/conf/config.yaml @@ -21,6 +21,7 @@ python_lib_path: - "/etc/localtime" - "/usr/share/zoneinfo" - "/etc/timezone" +uv_path: /usr/local/bin/uv enable_network: True # please make sure there is no network risk in your environment allowed_syscalls: # please leave it empty if you have no idea how seccomp works proxy: diff --git a/tests/integration_tests/init.go b/tests/integration_tests/init.go index 0e113062..4f13095a 100644 --- a/tests/integration_tests/init.go +++ b/tests/integration_tests/init.go @@ -1,7 +1,10 @@ package integrationtests_test import ( + "os" + "github.com/langgenius/dify-sandbox/internal/core/runner/python" + "github.com/langgenius/dify-sandbox/internal/core/runner/uv" "github.com/langgenius/dify-sandbox/internal/static" "github.com/langgenius/dify-sandbox/internal/utils/log" ) @@ -13,4 +16,23 @@ func init() { if err != nil { log.Panic("failed to initialize python dependencies sandbox: %v", err) } + + err = uv.PrepareUvDependenciesEnv() + if err != nil { + log.Panic("failed to initialize uv dependencies sandbox: %v", err) + } + + listSandboxFiles() +} + +func listSandboxFiles() { + dir := "/var/sandbox/sandbox-uv/" + files, err := os.ReadDir(dir) + if err != nil { + log.Panic("failed to read directory %s: %v", dir, err) + } + + for _, file := range files { + log.Info("File: %s", file.Name()) + } } diff --git a/tests/integration_tests/uv_feature_test.go b/tests/integration_tests/uv_feature_test.go new file mode 100644 index 00000000..d82612c7 --- /dev/null +++ b/tests/integration_tests/uv_feature_test.go @@ -0,0 +1,143 @@ +package integrationtests_test + +import ( + "strings" + "testing" + "time" + + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/service" +) + +func TestUvBase64(t *testing.T) { + // Test case for base64 + runMultipleTestings(t, 50, func(t *testing.T) { + resp := service.RunUvCode(` +import base64 +print(base64.b64decode(base64.b64encode(b"hello world")).decode()) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + if resp.Data.(*service.RunCodeResponse).Stderr != "" { + t.Fatalf("unexpected error: %s\n", resp.Data.(*service.RunCodeResponse).Stderr) + } + + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stdout, "hello world") { + t.Fatalf("unexpected output: %s\n", resp.Data.(*service.RunCodeResponse).Stdout) + } + }) +} + +func TestUvJSON(t *testing.T) { + runMultipleTestings(t, 50, func(t *testing.T) { + // Test case for json + resp := service.RunUvCode(` +import json +print(json.dumps({"hello": "world"})) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + if resp.Data.(*service.RunCodeResponse).Stderr != "" { + t.Fatalf("unexpected error: %s\n", resp.Data.(*service.RunCodeResponse).Stderr) + } + + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stdout, `{"hello": "world"}`) { + t.Fatalf("unexpected output: %s\n", resp.Data.(*service.RunCodeResponse).Stdout) + } + }) +} + +func TestUvRequests(t *testing.T) { + // Test case for http + runMultipleTestings(t, 1, func(t *testing.T) { + resp := service.RunUvCode(` +import requests +print(requests.get("https://www.bilibili.com").content) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + Dependenchies: []types.Dependency{ + { + Name: "requests", + Version: "2.32.3", + }, + }, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + if resp.Data.(*service.RunCodeResponse).Stderr != "" { + t.Fatalf("unexpected error: %s\n", resp.Data.(*service.RunCodeResponse).Stderr) + } + + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stdout, "bilibili") { + t.Fatalf("unexpected output: %s\n", resp.Data.(*service.RunCodeResponse).Stdout) + } + }) +} + +func TestUvHttpx(t *testing.T) { + // Test case for http + runMultipleTestings(t, 1, func(t *testing.T) { + resp := service.RunUvCode(` +import httpx +print(httpx.get("https://www.bilibili.com").content) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + Dependenchies: []types.Dependency{ + { + Name: "httpx", + }, + }, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + if resp.Data.(*service.RunCodeResponse).Stderr != "" { + t.Fatalf("unexpected error: %s\n", resp.Data.(*service.RunCodeResponse).Stderr) + } + + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stdout, "bilibili") { + t.Fatalf("unexpected output: %s\n", resp.Data.(*service.RunCodeResponse).Stdout) + } + }) +} + +func TestUvTimezone(t *testing.T) { + // Test case for time + runMultipleTestings(t, 1, func(t *testing.T) { + resp := service.RunUvCode(` +from datetime import datetime +from zoneinfo import ZoneInfo + +print(datetime.now(ZoneInfo("Asia/Shanghai")).isoformat()) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + if resp.Data.(*service.RunCodeResponse).Stderr != "" { + t.Fatalf("unexpected error: %s\n", resp.Data.(*service.RunCodeResponse).Stderr) + } + + stdout := resp.Data.(*service.RunCodeResponse).Stdout + // trim \n + stdout = strings.TrimSpace(stdout) + // check if stdout match time format + _, err := time.Parse("2006-01-02T15:04:05.000000+08:00", stdout) + if err != nil { + t.Fatalf("unexpected output: %s, error: %v\n", stdout, err) + } + }) +} diff --git a/tests/integration_tests/uv_longchars_test.go b/tests/integration_tests/uv_longchars_test.go new file mode 100644 index 00000000..c232001c --- /dev/null +++ b/tests/integration_tests/uv_longchars_test.go @@ -0,0 +1,62 @@ +package integrationtests_test + +import ( + "testing" + + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/service" +) + +func TestUvLargeOutput(t *testing.T) { + // Test case for base64 + runMultipleTestings(t, 5, func(t *testing.T) { + resp := service.RunUvCode(`# declare main function here +def main() -> dict: + original_strings_with_empty = ["apple", "", "cherry", "date", "", "fig", "grape", "honeydew", "kiwi", "", "mango", "nectarine", "orange", "papaya", "quince", "raspberry", "strawberry", "tangerine", "ugli fruit", "vanilla bean", "watermelon", "xigua", "yellow passionfruit", "zucchini"] * 5 + + extended_strings = [] + + for s in original_strings_with_empty: + if s: + repeat_times = 600 + extended_s = (s * repeat_times)[:3000] + extended_strings.append(extended_s) + else: + extended_strings.append(s) + + return { + "result": extended_strings, + } + +from json import loads, dumps +from base64 import b64decode + +# execute main function, and return the result +# inputs is a dict, and it +inputs = b64decode('e30=').decode('utf-8') +output = main(**loads(inputs)) + +# convert output to json and print +output = dumps(output, indent=4) + +result = f'''<> +{output} +<>''' + +print(result) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + if resp.Data.(*service.RunCodeResponse).Stderr != "" { + t.Fatalf("unexpected error: %s\n", resp.Data.(*service.RunCodeResponse).Stderr) + } + + if len(resp.Data.(*service.RunCodeResponse).Stdout) != 304487 { + t.Fatalf("unexpected output, expected 304487 bytes, got %d bytes\n", len(resp.Data.(*service.RunCodeResponse).Stdout)) + } + }) +} diff --git a/tests/integration_tests/uv_malicious_test.go b/tests/integration_tests/uv_malicious_test.go new file mode 100644 index 00000000..e9cef1e2 --- /dev/null +++ b/tests/integration_tests/uv_malicious_test.go @@ -0,0 +1,77 @@ +package integrationtests_test + +import ( + "strings" + "testing" + + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/service" +) + +func TestUvSysFork(t *testing.T) { + // Test case for sys_fork + resp := service.RunUvCode(` +import os +print(os.fork()) +print(123) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + + if resp.Code != 0 { + t.Error(resp) + } + + if resp.Data.(*service.RunCodeResponse).Stdout != "0\n123\n" { + t.Error(resp.Data.(*service.RunCodeResponse).Stderr) + } +} + +func TestUvExec(t *testing.T) { + // Test case for exec + resp := service.RunUvCode(` +import os +os.execl("/bin/ls", "ls") + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Error(resp) + } + + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "operation not permitted") { + t.Error(resp.Data.(*service.RunCodeResponse).Stderr) + } +} + +func TestUvRunCommand(t *testing.T) { + // Test case for run_command + resp := service.RunUvCode(` +import subprocess +subprocess.run(["ls", "-l"]) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Error(resp) + } + + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "operation not permitted") { + t.Error(resp.Data.(*service.RunCodeResponse).Stderr) + } +} + +func TestUvReadEtcPasswd(t *testing.T) { + resp := service.RunUvCode(` +print(open("/etc/passwd").read()) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Error(resp) + } + + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "No such file or directory") { + t.Error(resp.Data.(*service.RunCodeResponse).Stderr) + } +} From 8a34a1b8c4380e4633eef2eb3cbeff9dd094fa0a Mon Sep 17 00:00:00 2001 From: gouzi <530971494@qq.com> Date: Sun, 3 Aug 2025 11:20:54 +0800 Subject: [PATCH 2/3] clean code --- internal/static/config_default_arm64.go | 1 - tests/integration_tests/init.go | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/internal/static/config_default_arm64.go b/internal/static/config_default_arm64.go index 08e71849..28d24864 100644 --- a/internal/static/config_default_arm64.go +++ b/internal/static/config_default_arm64.go @@ -20,7 +20,6 @@ var DEFAULT_PYTHON_LIB_REQUIREMENTS = []string{ var DEFAULT_UV_LIB_REQUIREMENTS = []string{ "/root/.local/share/uv/python", - "/usr/lib/aarch64-linux-gnu", "/etc/ssl/certs/ca-certificates.crt", "/etc/nsswitch.conf", "/etc/hosts", diff --git a/tests/integration_tests/init.go b/tests/integration_tests/init.go index 4f13095a..aa5b8401 100644 --- a/tests/integration_tests/init.go +++ b/tests/integration_tests/init.go @@ -1,8 +1,6 @@ package integrationtests_test import ( - "os" - "github.com/langgenius/dify-sandbox/internal/core/runner/python" "github.com/langgenius/dify-sandbox/internal/core/runner/uv" "github.com/langgenius/dify-sandbox/internal/static" @@ -22,17 +20,4 @@ func init() { log.Panic("failed to initialize uv dependencies sandbox: %v", err) } - listSandboxFiles() -} - -func listSandboxFiles() { - dir := "/var/sandbox/sandbox-uv/" - files, err := os.ReadDir(dir) - if err != nil { - log.Panic("failed to read directory %s: %v", dir, err) - } - - for _, file := range files { - log.Info("File: %s", file.Name()) - } } From cf5472ecbc6c46ba307b9811ea420ed6cf406850 Mon Sep 17 00:00:00 2001 From: gouzi <530971494@qq.com> Date: Sun, 3 Aug 2025 11:28:20 +0800 Subject: [PATCH 3/3] fix: `UvMirrorURL` specified error --- internal/core/runner/uv/uv.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/core/runner/uv/uv.go b/internal/core/runner/uv/uv.go index 15aa3bf7..e6869be5 100644 --- a/internal/core/runner/uv/uv.go +++ b/internal/core/runner/uv/uv.go @@ -70,7 +70,9 @@ func (p *UvRunner) Run( if configuration.Proxy.Http != "" { cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_PROXY=%s", configuration.Proxy.Http)) } - } else if configuration.UvMirrorURL != "" { + } + + if configuration.UvMirrorURL != "" { cmd.Env = append(cmd.Env, fmt.Sprintf("UV_INDEX_URL=%s", configuration.UvMirrorURL)) }