snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
115
oryx/jsonnetsecure/cmd.go
Normal file
115
oryx/jsonnetsecure/cmd.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jsonnetsecure
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/ory/x/landlockx"
|
||||
)
|
||||
|
||||
const (
|
||||
GiB uint64 = 1024 * 1024 * 1024
|
||||
// Generous limit on virtual memory including the peak memory allocated by the Go runtime, the Jsonnet VM,
|
||||
// and the Jsonnet script.
|
||||
// This number was acquired by running:
|
||||
// Found by trial and error with:
|
||||
// `ulimit -Sv 1048576 && echo '{"Snippet": "{user_id: std.repeat(\'a\', 1000)}"}' | kratos jsonnet -0`
|
||||
// NOTE: Ideally we'd like to limit RSS but that is not possible on Linux with `ulimit/setrlimit(2)` - only with cgroups.
|
||||
virtualMemoryLimitBytes = 2 * GiB
|
||||
)
|
||||
|
||||
func NewJsonnetCmd() *cobra.Command {
|
||||
var null bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "jsonnet",
|
||||
Short: "Run Jsonnet as a CLI command",
|
||||
Hidden: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Lock the worker down to its already-open stdio: the
|
||||
// only I/O it does is read snippets/parameters from stdin
|
||||
// and write JSON to stdout/stderr. Denying all path-based
|
||||
// filesystem access prevents a malicious snippet from
|
||||
// touching the operator's file system even if the
|
||||
// jsonnet VM ever exposed a path-import primitive.
|
||||
if err := landlockx.ApplyEmpty(nil); err != nil {
|
||||
return errors.Wrap(err, "failed to apply empty landlock sandbox")
|
||||
}
|
||||
|
||||
// This could fail because current limits are lower than what we tried to set,
|
||||
// so we still continue in this case.
|
||||
SetVirtualMemoryLimit(virtualMemoryLimitBytes)
|
||||
|
||||
if null {
|
||||
return scan(cmd.OutOrStdout(), cmd.InOrStdin())
|
||||
}
|
||||
|
||||
input, err := io.ReadAll(cmd.InOrStdin())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read from stdin")
|
||||
}
|
||||
|
||||
json, err := eval(input)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to evaluate jsonnet")
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(cmd.OutOrStdout(), json); err != nil {
|
||||
return errors.Wrap(err, "failed to write json output")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVarP(&null, "null", "0", false,
|
||||
`Read multiple snippets and parameters from stdin separated by null bytes.
|
||||
Output will be in the same order as inputs, separated by null bytes.
|
||||
Evaluation errors will also be reported to stdout, separated by null bytes.
|
||||
Non-recoverable errors are written to stderr and the program will terminate with a non-zero exit code.`)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func scan(w io.Writer, r io.Reader) error {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(splitNull)
|
||||
for scanner.Scan() {
|
||||
json, err := eval(scanner.Bytes())
|
||||
if err != nil {
|
||||
json = fmt.Sprintf("ERROR: %s", err)
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s%c", json, 0); err != nil {
|
||||
return errors.Wrap(err, "failed to write json output")
|
||||
}
|
||||
}
|
||||
return errors.Wrap(scanner.Err(), "failed to read from stdin")
|
||||
}
|
||||
|
||||
func eval(input []byte) (json string, err error) {
|
||||
var params processParameters
|
||||
if err := params.Decode(input); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vm := MakeInProcessVM()
|
||||
|
||||
for _, it := range params.ExtCodes {
|
||||
vm.ExtCode(it.Key, it.Value)
|
||||
}
|
||||
for _, it := range params.ExtVars {
|
||||
vm.ExtVar(it.Key, it.Value)
|
||||
}
|
||||
for _, it := range params.TLACodes {
|
||||
vm.TLACode(it.Key, it.Value)
|
||||
}
|
||||
for _, it := range params.TLAVars {
|
||||
vm.TLAVar(it.Key, it.Value)
|
||||
}
|
||||
|
||||
return vm.EvaluateAnonymousSnippet(params.Filename, params.Snippet)
|
||||
}
|
||||
19
oryx/jsonnetsecure/cmd/root.go
Normal file
19
oryx/jsonnetsecure/cmd/root.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/ory/x/jsonnetsecure"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := jsonnetsecure.NewJsonnetCmd().ExecuteContext(context.Background()); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
153
oryx/jsonnetsecure/jsonnet.go
Normal file
153
oryx/jsonnetsecure/jsonnet.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package jsonnetsecure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-jsonnet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
VM interface {
|
||||
EvaluateAnonymousSnippet(filename string, snippet string) (json string, formattedErr error)
|
||||
ExtCode(key string, val string)
|
||||
ExtVar(key string, val string)
|
||||
TLACode(key string, val string)
|
||||
TLAVar(key string, val string)
|
||||
}
|
||||
|
||||
kv struct {
|
||||
Key, Value string
|
||||
}
|
||||
processParameters struct {
|
||||
Filename, Snippet string
|
||||
TLACodes, TLAVars, ExtCodes, ExtVars []kv
|
||||
}
|
||||
|
||||
vmOptions struct {
|
||||
jsonnetBinaryPath string
|
||||
args []string
|
||||
ctx context.Context
|
||||
pool *pool
|
||||
}
|
||||
|
||||
Option func(o *vmOptions)
|
||||
)
|
||||
|
||||
func (pp *processParameters) EncodeTo(w io.Writer) error {
|
||||
return json.NewEncoder(w).Encode(pp)
|
||||
}
|
||||
|
||||
func (pp *processParameters) Decode(d []byte) error {
|
||||
return json.Unmarshal(d, pp)
|
||||
}
|
||||
|
||||
func newVMOptions() *vmOptions {
|
||||
jsonnetBinaryPath, _ := os.Executable()
|
||||
return &vmOptions{
|
||||
jsonnetBinaryPath: jsonnetBinaryPath,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context) Option {
|
||||
return func(o *vmOptions) {
|
||||
o.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
func WithJsonnetBinary(jsonnetBinaryPath string) Option {
|
||||
return func(o *vmOptions) {
|
||||
o.jsonnetBinaryPath = jsonnetBinaryPath
|
||||
}
|
||||
}
|
||||
|
||||
func WithProcessArgs(args ...string) Option {
|
||||
return func(o *vmOptions) {
|
||||
o.args = args
|
||||
}
|
||||
}
|
||||
|
||||
// ErrNoProcessPool is returned by MakeSecureVM when called without a process
|
||||
// pool. It is a distinct error because the alternative — quietly returning an
|
||||
// in-process VM — would strip the isolation callers of this package rely on.
|
||||
var ErrNoProcessPool = errors.New("jsonnetsecure: a process pool is required; use MakeInProcessVM to evaluate in this process without isolation")
|
||||
|
||||
// MakeSecureVM returns a VM that evaluates snippets in a worker process taken
|
||||
// from p, so that a snippet which exhausts memory, spins on the CPU, or crashes
|
||||
// takes down only that worker.
|
||||
//
|
||||
// p is a required argument rather than an option because a VM without a pool
|
||||
// offers no isolation at all. Passing a nil pool returns ErrNoProcessPool.
|
||||
func MakeSecureVM(p Pool, opts ...Option) (VM, error) {
|
||||
// A nil *pool inside a non-nil Pool interface is not reachable from outside
|
||||
// this package (Pool has an unexported method), but check the concrete
|
||||
// value anyway so a future in-package mistake cannot slip through.
|
||||
concrete, _ := p.(*pool)
|
||||
if p == nil || concrete == nil {
|
||||
return nil, errors.WithStack(ErrNoProcessPool)
|
||||
}
|
||||
|
||||
options := newVMOptions()
|
||||
for _, o := range opts {
|
||||
o(options)
|
||||
}
|
||||
options.pool = concrete
|
||||
|
||||
return NewProcessPoolVM(options), nil
|
||||
}
|
||||
|
||||
// MakeInProcessVM returns a Jsonnet VM that evaluates in the calling process
|
||||
// with imports disabled. It provides no isolation: a malicious or buggy snippet
|
||||
// can exhaust this process's memory and CPU, so it is only safe for trusted
|
||||
// input. The two legitimate uses are the jsonnet subcommand, which is itself
|
||||
// the isolation boundary, and offline CLI linting. Everything that evaluates
|
||||
// tenant-supplied Jsonnet must use MakeSecureVM.
|
||||
func MakeInProcessVM() *jsonnet.VM {
|
||||
vm := jsonnet.MakeVM()
|
||||
vm.Importer(new(ErrorImporter))
|
||||
return vm
|
||||
}
|
||||
|
||||
// ErrorImporter errors when calling "import".
|
||||
type ErrorImporter struct{}
|
||||
|
||||
// Import fetches data from a map entry.
|
||||
// All paths are treated as absolute keys.
|
||||
func (importer *ErrorImporter) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) {
|
||||
return jsonnet.Contents{}, "", fmt.Errorf("import not available %v", importedPath)
|
||||
}
|
||||
|
||||
func JsonnetTestBinary(t testing.TB) string {
|
||||
t.Helper()
|
||||
|
||||
// We can force the usage of a given jsonnet executable.
|
||||
// Useful to test different versions, or run the tests under wine.
|
||||
if s := os.Getenv("ORY_JSONNET_PATH"); s != "" {
|
||||
return s
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
// Using `t.TempDir()` results in permissions errors on Windows, sometimes.
|
||||
outPath := path.Join(os.TempDir(), "jsonnet")
|
||||
if runtime.GOOS == "windows" {
|
||||
outPath = outPath + ".exe"
|
||||
}
|
||||
cmd := exec.Command("go", "build", "-o", outPath, "github.com/ory/x/jsonnetsecure/cmd")
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil || stderr.Len() != 0 {
|
||||
t.Fatalf("building the Go binary returned error: %v\n%s", err, stderr.String())
|
||||
}
|
||||
|
||||
return outPath
|
||||
}
|
||||
313
oryx/jsonnetsecure/jsonnet_pool.go
Normal file
313
oryx/jsonnetsecure/jsonnet_pool.go
Normal file
@@ -0,0 +1,313 @@
|
||||
// Copyright © 2024 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jsonnetsecure
|
||||
|
||||
// Known limitations/edge cases:
|
||||
// - The child process exiting early (e.g. crashing) or getting killed (e.g. reaching some OS limit)
|
||||
// is not detected and no error will be returned in this case from `eval()`.
|
||||
// - Misbehaving jsonnet scripts in the middle of a batch being passed to the child process for evaluation may result in
|
||||
// no error (as mentioned above), and other valid scripts in this batch may result
|
||||
// in an error (because the output from the child process is truncated).
|
||||
//
|
||||
// Possible remediations:
|
||||
// - Do not pass a batch of scripts to a worker, only pass one script at a time (to isolate misbehaving scripts)
|
||||
// - Validate that the output is valid JSON (to detect truncated output)
|
||||
// - Detect the child process exiting (to return an error)
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/puddle/v2"
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/ory/x/otelx"
|
||||
)
|
||||
|
||||
const (
|
||||
KiB = 1024
|
||||
jsonnetOutputLimit = 256 * KiB
|
||||
jsonnetErrLimit = 1 * KiB
|
||||
)
|
||||
|
||||
type (
|
||||
processPoolVM struct {
|
||||
path string
|
||||
args []string
|
||||
ctx context.Context
|
||||
params processParameters
|
||||
pool *pool
|
||||
}
|
||||
Pool interface {
|
||||
Close()
|
||||
// Stat returns a snapshot of the worker pool's statistics.
|
||||
Stat() *puddle.Stat
|
||||
private()
|
||||
}
|
||||
pool struct {
|
||||
puddle *puddle.Pool[worker]
|
||||
}
|
||||
worker struct {
|
||||
cmd *exec.Cmd
|
||||
stdin chan<- []byte
|
||||
stdout <-chan string
|
||||
stderr <-chan string
|
||||
}
|
||||
contextKeyType string
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProcessPoolClosed = errors.New("jsonnetsecure: process pool closed")
|
||||
|
||||
_ VM = (*processPoolVM)(nil)
|
||||
_ Pool = (*pool)(nil)
|
||||
|
||||
contextValuePath contextKeyType = "argc"
|
||||
contextValueArgs contextKeyType = "argv"
|
||||
)
|
||||
|
||||
func NewProcessPool(size int) Pool {
|
||||
size = max(5, min(size, math.MaxInt32))
|
||||
pud, err := puddle.NewPool(&puddle.Config[worker]{
|
||||
MaxSize: int32(size), //nolint:gosec // disable G115 // because of the previous min/max, 5 <= size <= math.MaxInt32
|
||||
Constructor: newWorker,
|
||||
Destructor: worker.destroy,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err) // this should never happen, see implementation of puddle.NewPool
|
||||
}
|
||||
for range size {
|
||||
// warm pool
|
||||
go pud.CreateResource(context.Background())
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(10 * time.Second)
|
||||
for _, proc := range pud.AcquireAllIdle() {
|
||||
if proc.Value().cmd.ProcessState != nil {
|
||||
proc.Destroy()
|
||||
} else {
|
||||
proc.Release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return &pool{pud}
|
||||
}
|
||||
|
||||
func (*pool) private() {}
|
||||
|
||||
func (p *pool) Close() {
|
||||
p.puddle.Close()
|
||||
}
|
||||
|
||||
func (p *pool) Stat() *puddle.Stat {
|
||||
return p.puddle.Stat()
|
||||
}
|
||||
|
||||
func newWorker(ctx context.Context) (_ worker, err error) {
|
||||
tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("")
|
||||
ctx, span := tracer.Start(ctx, "jsonnetsecure.newWorker")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
path, _ := ctx.Value(contextValuePath).(string)
|
||||
if path == "" {
|
||||
return worker{}, errors.New("newWorker: missing binary path in context")
|
||||
}
|
||||
args, _ := ctx.Value(contextValueArgs).([]string)
|
||||
cmd := exec.Command(path, append(args, "-0")...)
|
||||
cmd.Env = []string{"GOMAXPROCS=1"}
|
||||
cmd.WaitDelay = 100 * time.Millisecond
|
||||
|
||||
span.SetAttributes(semconv.ProcessCommand(cmd.Path), semconv.ProcessCommandArgs(cmd.Args...))
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return worker{}, errors.Wrap(err, "newWorker: failed to create stdin pipe")
|
||||
}
|
||||
|
||||
in := make(chan []byte, 1)
|
||||
go func(c <-chan []byte) {
|
||||
for input := range c {
|
||||
if _, err := stdin.Write(append(input, 0)); err != nil {
|
||||
stdin.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}(in)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return worker{}, errors.Wrap(err, "newWorker: failed to create stdout pipe")
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return worker{}, errors.Wrap(err, "newWorker: failed to create stderr pipe")
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return worker{}, errors.Wrap(err, "newWorker: failed to start process")
|
||||
}
|
||||
|
||||
span.SetAttributes(semconv.ProcessPID(cmd.Process.Pid))
|
||||
|
||||
scan := func(c chan<- string, r io.Reader, maxTokenSize int) {
|
||||
defer close(c)
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Buffer(make([]byte, 0, 64*KiB), maxTokenSize)
|
||||
|
||||
scanner.Split(splitNull)
|
||||
for scanner.Scan() {
|
||||
c <- scanner.Text()
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
c <- "ERROR: scan: " + err.Error()
|
||||
}
|
||||
}
|
||||
out := make(chan string, 1)
|
||||
go scan(out, stdout, jsonnetOutputLimit)
|
||||
errs := make(chan string, 1)
|
||||
go scan(errs, stderr, jsonnetErrLimit)
|
||||
|
||||
w := worker{
|
||||
cmd: cmd,
|
||||
stdin: in,
|
||||
stdout: out,
|
||||
stderr: errs,
|
||||
}
|
||||
_, err = w.eval(ctx, []byte("{}")) // warm up
|
||||
if err != nil {
|
||||
w.destroy()
|
||||
return worker{}, errors.Wrap(err, "newWorker: warm up failed")
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (w worker) destroy() {
|
||||
close(w.stdin)
|
||||
w.cmd.Process.Kill()
|
||||
w.cmd.Wait()
|
||||
}
|
||||
|
||||
func (w worker) eval(ctx context.Context, processParams []byte) (output string, err error) {
|
||||
tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("")
|
||||
ctx, span := tracer.Start(ctx, "jsonnetsecure.worker.eval", trace.WithAttributes(
|
||||
semconv.ProcessPID(w.cmd.Process.Pid)))
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
// The worker is exclusively acquired, so the schedstat delta over this
|
||||
// evaluation is attributable to it. cpu_time_us is time actually spent on
|
||||
// a CPU; runqueue_wait_us is time runnable but waiting for one. Span wall
|
||||
// time far exceeding their sum means the process was not even runnable,
|
||||
// e.g. dequeued by cgroup CPU throttling.
|
||||
//
|
||||
// A kernel without CONFIG_SCHED_INFO reports "0 0 0" instead of failing
|
||||
// the read. The worker has already spent CPU time on its warm-up
|
||||
// evaluation, so a zero cpuTime means the fields are dead — skip them
|
||||
// rather than record misleading zeros.
|
||||
if before, ok := readSchedstat(w.cmd.Process.Pid); ok && before.cpuTime > 0 {
|
||||
defer func() {
|
||||
after, ok := readSchedstat(w.cmd.Process.Pid)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.Int64("jsonnet.worker.cpu_time_us", (after.cpuTime-before.cpuTime).Microseconds()),
|
||||
attribute.Int64("jsonnet.worker.runqueue_wait_us", (after.runqueueWait-before.runqueueWait).Microseconds()),
|
||||
)
|
||||
}()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case w.stdin <- processParams:
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case output := <-w.stdout:
|
||||
return output, nil
|
||||
case err := <-w.stderr:
|
||||
return "", errors.New(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (vm *processPoolVM) EvaluateAnonymousSnippet(filename string, snippet string) (_ string, err error) {
|
||||
tracer := trace.SpanFromContext(vm.ctx).TracerProvider().Tracer("")
|
||||
ctx, span := tracer.Start(vm.ctx, "jsonnetsecure.processPoolVM.EvaluateAnonymousSnippet", trace.WithAttributes(attribute.String("filename", filename)))
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
params := vm.params
|
||||
params.Filename = filename
|
||||
params.Snippet = snippet
|
||||
pp, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "jsonnetsecure: marshal")
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, contextValuePath, vm.path)
|
||||
ctx = context.WithValue(ctx, contextValueArgs, vm.args)
|
||||
worker, err := vm.pool.puddle.Acquire(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "jsonnetsecure: acquire")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeoutCause(ctx, 1*time.Second, errors.Errorf("failed to run jsonnet within 1s: filename=%s", filename))
|
||||
defer cancel()
|
||||
result, err := worker.Value().eval(ctx, pp)
|
||||
if err != nil {
|
||||
worker.Destroy()
|
||||
return "", errors.Wrap(err, "jsonnetsecure: eval")
|
||||
} else {
|
||||
worker.Release()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(result, "ERROR: ") {
|
||||
return "", errors.New("jsonnetsecure: " + result)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NewProcessPoolVM(opts *vmOptions) VM {
|
||||
ctx := opts.ctx
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return &processPoolVM{
|
||||
path: opts.jsonnetBinaryPath,
|
||||
args: opts.args,
|
||||
ctx: ctx,
|
||||
pool: opts.pool,
|
||||
}
|
||||
}
|
||||
|
||||
func (vm *processPoolVM) ExtCode(key string, val string) {
|
||||
vm.params.ExtCodes = append(vm.params.ExtCodes, kv{key, val})
|
||||
}
|
||||
|
||||
func (vm *processPoolVM) ExtVar(key string, val string) {
|
||||
vm.params.ExtVars = append(vm.params.ExtVars, kv{key, val})
|
||||
}
|
||||
|
||||
func (vm *processPoolVM) TLACode(key string, val string) {
|
||||
vm.params.TLACodes = append(vm.params.TLACodes, kv{key, val})
|
||||
}
|
||||
|
||||
func (vm *processPoolVM) TLAVar(key string, val string) {
|
||||
vm.params.TLAVars = append(vm.params.TLAVars, kv{key, val})
|
||||
}
|
||||
29
oryx/jsonnetsecure/limit_unix.go
Normal file
29
oryx/jsonnetsecure/limit_unix.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright © 2025 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package jsonnetsecure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func SetVirtualMemoryLimit(limitBytes uint64) error {
|
||||
// Tell the Go runtime about the limit.
|
||||
debug.SetMemoryLimit(int64(limitBytes)) //nolint:gosec // The number is a compile-time constant.
|
||||
|
||||
lim := syscall.Rlimit{
|
||||
Cur: limitBytes,
|
||||
Max: limitBytes,
|
||||
}
|
||||
err := syscall.Setrlimit(syscall.RLIMIT_AS, &lim)
|
||||
if err != nil {
|
||||
return errors.WithStack(fmt.Errorf("failed to set virtual memory limit: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
16
oryx/jsonnetsecure/limit_windows.go
Normal file
16
oryx/jsonnetsecure/limit_windows.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright © 2025 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build windows
|
||||
|
||||
package jsonnetsecure
|
||||
|
||||
import "runtime/debug"
|
||||
|
||||
func SetVirtualMemoryLimit(limitBytes uint64) error {
|
||||
// Tell the Go runtime about the limit.
|
||||
debug.SetMemoryLimit(int64(limitBytes)) //nolint:gosec // The number is a compile-time constant.
|
||||
|
||||
// TODO No OS limit for now. Apparently there is a Windows-specific equivalent (Job control)?
|
||||
return nil
|
||||
}
|
||||
22
oryx/jsonnetsecure/null.go
Normal file
22
oryx/jsonnetsecure/null.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright © 2024 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jsonnetsecure
|
||||
|
||||
import "bytes"
|
||||
|
||||
func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
// Look for a null byte; if found, return the position after it,
|
||||
// the data before it, and no error.
|
||||
if i := bytes.IndexByte(data, 0); i >= 0 {
|
||||
return i + 1, data[0:i], nil
|
||||
}
|
||||
|
||||
// If we're at EOF, we have a final, non-terminated word. Return it.
|
||||
if atEOF && len(data) != 0 {
|
||||
return len(data), data, nil
|
||||
}
|
||||
|
||||
// Request more data.
|
||||
return 0, nil, nil
|
||||
}
|
||||
60
oryx/jsonnetsecure/provider.go
Normal file
60
oryx/jsonnetsecure/provider.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jsonnetsecure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type (
|
||||
VMProvider interface {
|
||||
// JsonnetVM creates a new secure process-isolated Jsonnet VM whose
|
||||
// execution is bound to the provided context, i.e.,
|
||||
// cancelling the context will terminate the VM process.
|
||||
JsonnetVM(context.Context) (VM, error)
|
||||
}
|
||||
|
||||
// TestProvider provides a secure VM by running go build on github.com/ory/x/jsonnetsecure/cmd
|
||||
TestProvider struct {
|
||||
jsonnetBinary string
|
||||
pool Pool
|
||||
}
|
||||
|
||||
// DefaultProvider provides a secure VM by calling the currently
|
||||
// running the current binary with the provided subcommand.
|
||||
DefaultProvider struct {
|
||||
Subcommand string
|
||||
Pool Pool
|
||||
}
|
||||
)
|
||||
|
||||
func NewTestProvider(t testing.TB) *TestProvider {
|
||||
pool := NewProcessPool(runtime.GOMAXPROCS(0))
|
||||
t.Cleanup(pool.Close)
|
||||
return &TestProvider{JsonnetTestBinary(t), pool}
|
||||
}
|
||||
|
||||
func (p *TestProvider) JsonnetVM(ctx context.Context) (VM, error) {
|
||||
return MakeSecureVM(
|
||||
p.pool,
|
||||
WithContext(ctx),
|
||||
WithJsonnetBinary(p.jsonnetBinary),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *DefaultProvider) JsonnetVM(ctx context.Context) (VM, error) {
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return MakeSecureVM(
|
||||
p.Pool,
|
||||
WithContext(ctx),
|
||||
WithJsonnetBinary(self),
|
||||
WithProcessArgs(p.Subcommand),
|
||||
)
|
||||
}
|
||||
53
oryx/jsonnetsecure/schedstat.go
Normal file
53
oryx/jsonnetsecure/schedstat.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jsonnetsecure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// schedstat holds the scheduler statistics of a process as reported by the
|
||||
// Linux kernel in /proc/<pid>/schedstat: the CPU time the process actually
|
||||
// spent running, and the time it spent runnable but waiting for a CPU (due to
|
||||
// contention or cgroup CPU throttling).
|
||||
type schedstat struct {
|
||||
cpuTime time.Duration
|
||||
runqueueWait time.Duration
|
||||
}
|
||||
|
||||
// readSchedstat returns the cumulative scheduler statistics for pid. It
|
||||
// reports ok=false on platforms without /proc/<pid>/schedstat (e.g. macOS,
|
||||
// Windows) and on Linux kernels built without CONFIG_SCHED_INFO.
|
||||
func readSchedstat(pid int) (schedstat, bool) {
|
||||
raw, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/schedstat")
|
||||
if err != nil {
|
||||
return schedstat{}, false
|
||||
}
|
||||
return parseSchedstat(raw)
|
||||
}
|
||||
|
||||
// parseSchedstat parses the /proc/<pid>/schedstat format: three
|
||||
// space-separated integers (run time in nanoseconds, runqueue wait time in
|
||||
// nanoseconds, number of timeslices).
|
||||
func parseSchedstat(raw []byte) (schedstat, bool) {
|
||||
fields := bytes.Fields(raw)
|
||||
if len(fields) < 2 {
|
||||
return schedstat{}, false
|
||||
}
|
||||
cpuTime, err := strconv.ParseInt(string(fields[0]), 10, 64)
|
||||
if err != nil || cpuTime < 0 {
|
||||
return schedstat{}, false
|
||||
}
|
||||
runqueueWait, err := strconv.ParseInt(string(fields[1]), 10, 64)
|
||||
if err != nil || runqueueWait < 0 {
|
||||
return schedstat{}, false
|
||||
}
|
||||
return schedstat{
|
||||
cpuTime: time.Duration(cpuTime),
|
||||
runqueueWait: time.Duration(runqueueWait),
|
||||
}, true
|
||||
}
|
||||
1
oryx/jsonnetsecure/stub/import.jsonnet
Normal file
1
oryx/jsonnetsecure/stub/import.jsonnet
Normal file
@@ -0,0 +1 @@
|
||||
{ foo: 'bar' }
|
||||
Reference in New Issue
Block a user