snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (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:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

115
oryx/jsonnetsecure/cmd.go Normal file
View 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)
}

View 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)
}
}

View 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
}

View 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})
}

View 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
}

View 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
}

View 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
}

View 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),
)
}

View 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
}

View File

@@ -0,0 +1 @@
{ foo: 'bar' }