emrun

package module
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 20, 2026 License: MIT Imports: 17 Imported by: 1

README

emrun

Run embedded executables and scripts straight from anonymous memory on Linux and Android, or from temporary files on any platform. emrun wraps memfd_create(2) so you can bundle auxiliary tooling and scripts inside a Go binary, execute them without touching disk in the common case, and keep the package fully self-contained even when hardened kernels restrict anonymous execution. The companion package efrun exposes the same API surface but always executes from a temporary file, which makes it portable to platforms where memfd_create is unavailable, but you have to explicitly choose that import.

Features

  • Creates an anonymous executable file descriptor whose name is the payload SHA-256, making it easy to trace in /proc/<pid>/fd.
  • Runs raw byte payloads, inline scripts, or strings with Run, RunIO, RunIOE, and Do.
  • Works seamlessly with Go's //go:embed, enabling you to ship extra binaries or shell helpers in a single distributable.
  • Automatically falls back to a temporary on-disk executable (deleted on close) when security policies forbid running the anonymous file descriptor.
  • Enforces context-driven SHA-256 allow/deny policies so bundled or perhaps downloaded payloads obey explicit checksum rules before execution.
  • Swap in efrun when you need the same interface without memfd support—for example on Windows or macOS, or when policies forbid anonymous execution and you prefer to opt out of the memfd attempt entirely.

Installation

go get pkt.systems/emrun

The emrun package itself only builds on Linux and Android (see the build tag at the top of emrun.go). The module also provides pkt.systems/emrun/efrun, which compiles on any platform and mirrors the same helpers without using memfd_create.

Quick Start

Below is the smallest example. The payload here is an inline shell script, but all helpers return the same experience even when backed by //go:embed.

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "pkt.systems/emrun"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    payload := []byte("#!/bin/sh\necho quick-start\n")

    out, err := emrun.Run(ctx, payload)
    if err != nil {
        log.Fatalf("run failed: %v", err)
    }
    fmt.Printf("payload said: %s", out)
}

Using //go:embed With Binaries

The primary use-case is bundling helper binaries so that the top-level Go program ships as a single artifact. Start by embedding your binary as a byte slice and then execute it with Run or RunIO.

package main

import (
    "context"
    "embed"
    "fmt"
    "log"
    "time"

    "pkt.systems/emrun"
)

//go:embed bin/busybox
var busybox []byte

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // BusyBox expects the applet name as the first argument when invoked as
    // `busybox <applet>`. The memfd path will still be argv[0].
    out, err := emrun.Run(ctx, busybox, "echo", "hello from busybox")
    if err != nil {
        log.Fatalf("busybox run failed: %v", err)
    }

    fmt.Printf("busybox output:\n%s", out)
}

Notes:

  • The payload must be executable for Linux; if you embed an ELF binary, ensure it targets the same architecture as the host machine.
  • Run returns combined stdout and stderr. RunIO mirrors both streams into a single writer, while RunIOE accepts separate writers for stdout and stderr.
  • When a fallback is required, the payload is written to a 0700 temporary file under the current user's default temp directory. Close() removes it.

Embedding Scripts With //go:embed

Shell scripts can also be embedded, which keeps helper logic tidy and separate from the Go source.

package script

import (
    "context"
    "embed"
    "log"
    "os"
    "time"

    "pkt.systems/emrun"
)

//go:embed scripts/upgrade.sh
var upgradeScript []byte

func RunUpgrade(ctx context.Context, args ...string) error {
    // Stream output directly to the parent process so progress is visible.
    return emrun.RunIO(ctx, nil, os.Stdout, upgradeScript, args...)
}

From the caller you can pass a context with a deadline or cancel when shutting down.

Inline Script Helpers

Sometimes writing the payload in Go is more convenient. Do accepts a string, which is ideal for small wrappers or ad-hoc tasks.

out, err := emrun.Do(
    ctx, `#!/bin/sh
now=$(date +%s)
echo "inline timestamp: $now"
`)
if err != nil {
    return err
}
fmt.Print(string(out))

For more control, use RunIO with custom readers/writers. The example below pipes data in and captures combined output through a single buffer. Choose RunIOE when you need to keep stdout and stderr separate without shell tricks.

var combined bytes.Buffer
err := emrun.RunIO(
    ctx,
    strings.NewReader("payload\n"),
    &combined,
    []byte("#!/bin/sh\nread line\nprintf 'seen:%s\\n' \"$line\"\n"),
)
if err != nil {
    return err
}
log.Printf("script said %q", combined.String())

Enforcing SHA-256 Policies

emrun can restrict which embedded payloads run by attaching digest policies to the context.Context. Set a default verdict with WithPolicy, then register explicit allow or deny entries with WithRule. Inputs may be raw hex strings, [32]byte values, or the contents of a sha256sum file—filenames are ignored.

ctx := emrun.WithPolicy(ctx, emrun.DENY) // default to deny unknown payloads

// Load hashes from an embedded sha256sum file or explicit strings.
ctx = emrun.WithRule(ctx, emrun.ALLOW, embeddedChecksums, "b09864fcb9...")

// Run helpers consult the policy automatically.
out, err := emrun.Run(ctx, payload)

// Manual checks are available when wiring custom execution paths.
digest := sha256.Sum256(payload)
if err := emrun.CheckPolicy(ctx, digest, hex.EncodeToString(digest[:])); err != nil {
    return err
}

Use WithRuleCatchError when you need to surface parse errors instead of panicking—handy if the checksum material comes from user input or config files.

Background Execution

RunBG, RunIOBG, RunIOEBG, and DoBG launch payloads asynchronously and return a Background handle. The handle exposes the running Context, a Cancel function, and a Done channel that delivers a Result. Combined output is only captured for RunBG/DoBG; streaming variants return nil in Result.CombinedOutput because stdout/stderr are already wired to callers.

Simple example that runs one payload in the background and waits for it:

bg, err := emrun.RunBG(ctx, payload, "--task")
if err != nil {
    return err
}
defer bg.Cancel()

select {
case res := <-bg.Done:
    if res.Error != nil {
        return res.Error
    }
    fmt.Printf("exit=%d output=%s", res.ExitCode, res.CombinedOutput)
case <-ctx.Done():
    return ctx.Err()
}

Running multiple background commands concurrently:

bg1, err := emrun.RunBG(ctx, payload1)
if err != nil {
    return err
}
bg2, err := emrun.RunIOBG(ctx, nil, os.Stdout, payload2)
if err != nil {
    bg1.Cancel()
    return err
}
defer bg1.Cancel()
defer bg2.Cancel()

results := make(chan *emrun.Result, 2)
collect := func(name string, bg *emrun.Background) {
    res := bg.WaitWithContext(ctx)
    if res.Error != nil {
        log.Printf("%s failed: %v", name, res.Error)
    } else {
        log.Printf("%s exit=%d", name, res.ExitCode)
    }
    results <- &res
}

go collect("job1", bg1)
go collect("job2", bg2)

var firstErr error
for i := 0; i < 2; i++ {
    res := <-results
    if res.Error != nil && firstErr == nil {
        firstErr = res.Error
    }
}
if firstErr != nil {
    return firstErr
}

Or coordinating a dynamic set of background jobs:

backgrounds := make([]*emrun.Background, 0, len(payloads))
for i, payload := range payloads {
    bg, err := emrun.RunBG(ctx, payload, fmt.Sprintf("--job=%d", i))
    if err != nil {
        for _, b := range backgrounds {
            b.Cancel()
        }
        return err
    }
    backgrounds = append(backgrounds, bg)
}
defer func() {
    for _, bg := range backgrounds {
        bg.Cancel()
    }
}()

for _, bg := range backgrounds {
    select {
    case res := <-bg.Done:
        if res.Error != nil {
            return res.Error
        }
    case <-ctx.Done():
        return ctx.Err()
    }
}

The same helpers exist in efrun if you need the portable backend.

Selecting a Runner

Both runners expose the same helpers: Open, Run, RunIO, RunIOE, Do, and their background equivalents. Switching between them is one import change, making it easy to choose the execution strategy per build.

  • pkt.systems/emrun (Linux/Android only) prefers anonymous execution via memfd_create and auto-falls back to a secure temporary file when necessary.
  • pkt.systems/emrun/efrun (portable) always writes a temporary file.

Internally, both return a port.Runnable with a shared Run method so you can depend on the interface in your own abstractions.

Build-Tagged Imports

If your program targets both Linux/Android and other operating systems, make the runner choice explicit with build tags. The emrun package will not compile on non-Linux targets, so cross-compiles fail unless you import efrun on those platforms (this was a design-choice). The pattern below keeps call sites identical while letting the Go toolchain select the correct backend automatically:

// runner_linux.go
//go:build linux || android
// +build linux android

package runner

import (
    runner "pkt.systems/emrun"
)
// runner_other.go
//go:build !linux && !android
// +build !linux,!android

package runner

import (
    runner "pkt.systems/emrun/efrun"
)

Everywhere else, depend on the shared runner alias (or whichever name you prefer) and call runner.Run, runner.Do, etc. When you build with GOOS=linux or GOOS=android, the first file is compiled; all other targets use the second.

Execution Flow (emrun)

  1. Open calls memfd_create with the SHA-256 digest as the name, writes the payload into the anonymous file, and returns a handle pointing at /proc/self/fd/<n>.
  2. Run, RunIO, and Do call Open, execute using exec.CommandContext, and close the file descriptor after the process exits.
  3. If the kernel refuses to execute the anonymous file (for example SELinux or AppArmor report EACCES/EPERM), the payload is atomically copied to a secure temporary file, permissioned 0700, and the command is retried from disk. Temporary files are removed automatically on Close().

Because the backing file exists only in memory in the common case, nothing ever hits disk. The file descriptor is closed immediately after successful execution, keeping the footprint small. When you import efrun, only step 3 applies—the payload is written to a temporary file immediately and executed from disk.

Caveats and Platform Notes

  • SELinux / AppArmor: Some distributions ship with policies that forbid executing anonymous memory (for example execmem, execmod, or memfd_exec). Tight policies may block the child process from starting or log AVC denials. emrun detects permission denials and retries from a temporary file automatically, but you should still validate under the target policy and ensure the temp directory is acceptable for your threat model.
  • Android: Android kernels support memfd_create, but application sandboxes and seccomp filters can forbid executing anonymous memory. Apps running in the default untrusted app domain may hit permission denials, so verify on the minimum API level you need.
  • Kernel version: memfd_create requires Linux 3.17+. Older LTS kernels or restricted containers may not provide it. The package surfaces the original error from unix.MemfdCreate so you can detect unsupported hosts.
  • Debugging and tooling: Since payloads never land on disk, external tools cannot read them post-mortem. Be sure to keep a copy of the original assets if you rely on crash dumps or static analysis.
  • Resource management: Open callers must close the returned file when they stop using it. The helper functions handle this automatically, but remember to close files when using Open directly.

License

MIT

Documentation

Overview

Run embedded executables and scripts straight from anonymous memory on Linux and Android. emrun wraps memfd_create(2) so you can bundle auxiliary tooling and scripts inside a Go binary, execute them without touching disk in the common case, and keep the package fully self-contained even when hardened kernels restrict anonymous execution.

Index

Constants

This section is empty.

Variables

View Source
var (
	ERR_PAYLOAD_IS_EMPTY   error = errors.New("payload is empty")
	ERR_NOT_AN_INMEMORY_FD error = errors.New("not an in-memory file descriptor")
)
View Source
var ErrDenied = errors.New("emrun: execution denied by policy")

Functions

func CheckPolicy

func CheckPolicy(ctx context.Context, digest [32]byte, hexDigest string) error

CheckPolicy inspects the context policy and returns ErrDenied if the digest violates the configured rules.

ctx := emrun.WithRule(context.Background(), emrun.DENY, "deadbeef...")
digest := sha256.Sum256(payload)
if err := emrun.CheckPolicy(ctx, digest, hex.EncodeToString(digest[:])); err != nil {
	return err
}

func Do

func Do(ctx context.Context, payload string, arg ...string) ([]byte, error)

Do is intended to run shebang scripts inline or from string vars. Uses ctx in exec.CommandContext and returns (*exec.Cmd).CombinedOutput.

func Run

func Run(ctx context.Context, executablePayload []byte, arg ...string) ([]byte, error)

Run executes the payload with ctx in exec.CommandContext with args using (*exec.Cmd).CombinedOutput, returns combined output or error. cmd.Stdin is nil, use RunIO if you want to pass data via stdin.

func RunCommand

func RunCommand(runner port.CommandRunner, cmd *exec.Cmd, combinedOutput bool) ([]byte, error)

RunCommand executes cmd using the supplied runner. When combinedOutput is true the function captures stdout and stderr into a shared buffer and returns it as a copy to the caller. Otherwise RunCommand defers to the runner without altering the configured streams.

func RunIO

func RunIO(ctx context.Context, r io.Reader, w io.Writer, executablePayload []byte, arg ...string) error

RunIO is similar to Run but uses r for stdin and w for stdout and stderr. Uses ctx for (*exec.Cmd).CommandContext.

func RunIOE

func RunIOE(ctx context.Context, r io.Reader, stdout io.Writer, stderr io.Writer, executablePayload []byte, arg ...string) error

RunIOE is exactly like RunIO except with separate stdout and stderr writers.

func StartCommand

func StartCommand(runner port.CommandRunner, cmd *exec.Cmd, combinedOutput bool) (port.CommandCapture, error)

StartCommand starts cmd using the supplied runner while optionally capturing combined stdout/stderr. The returned CommandCapture must later be passed to WaitCommand (or Restore via Finish) to release resources.

func WithPolicy

func WithPolicy(ctx context.Context, verdict Verdict) context.Context

WithPolicy returns a derived context that sets the default verdict consulted when no explicit allow/deny rule matches a payload digest.

ctx := emrun.WithPolicy(context.Background(), emrun.DENY)
ctx = emrun.WithRule(ctx, emrun.ALLOW, sha256FileBytes)
_ = emrun.CheckPolicy(ctx, digest, hexDigest)

func WithRule

func WithRule(ctx context.Context, rule Verdict, sha256Digests ...Digest) context.Context

WithRule returns a derived context containing explicit allow/deny entries for SHA-256 digests. Each argument may be a raw digest type (string, []byte, [32]byte) or sha256sum-formatted content; filenames are ignored. WithRule must succeed - invalid input causes a panic.

ctx := emrun.WithPolicy(ctx, emrun.DENY)
ctx = emrun.WithRule(ctx, emrun.ALLOW, []byte("<digest>  tool"))
ctx = emrun.WithRule(ctx, emrun.DENY, "deadbeef...deadbeef")
_ = emrun.CheckPolicy(ctx, digest, hexDigest)

func WithRuleCatchError

func WithRuleCatchError(ctx context.Context, rule Verdict, sha256Digests ...Digest) (context.Context, error)

WithRuleCatchError mirrors WithRule but returns an error instead of panicking when digest parsing fails or an unsupported verdict is supplied.

Types

type Background

type Background struct {
	Context context.Context
	Cancel  context.CancelFunc
	Done    <-chan Result
}

func DoBG

func DoBG(ctx context.Context, payload string, arg ...string) (*Background, error)

DoBG runs the provided script string in the background, mirroring Do but returning a Background handle so callers can select on completion or cancel.

func RunBG

func RunBG(ctx context.Context, executablePayload []byte, arg ...string) (*Background, error)

RunBG launches the payload in the background and returns a Background handle that exposes the running context. Example usage:

bg, err := emrun.RunBG(ctx, payload, "--flag")
if err != nil {
	return err
}
defer bg.Cancel()
select {
case res := <-bg.Done:
	if res.Error != nil {
		return res.Error
	}
case <-ctx.Done():
	return ctx.Err()
}

func RunIOBG

func RunIOBG(ctx context.Context, reader io.Reader, writer io.Writer, executablePayload []byte, arg ...string) (*Background, error)

RunIOBG behaves like RunBG but wires the provided reader/writer to stdin and combined stdout/stderr. The returned Result has a nil CombinedOutput since output is streamed to writer.

func RunIOEBG

func RunIOEBG(ctx context.Context, reader io.Reader, stdout io.Writer, stderr io.Writer, executablePayload []byte, arg ...string) (*Background, error)

RunIOEBG is the background variant of RunIOE, streaming stdout and stderr to separate writers while returning a Background handle for lifecycle control.

func StartBackground

func StartBackground(parentCtx context.Context, run port.BackgroundRunnable, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, combined bool) (*Background, error)

StartBackground launches cmd via the runnable, wiring optional stdio streams and returning a Background handle that reports completion through Done.

func (*Background) Wait

func (bg *Background) Wait() Result

Wait blocks until the background command finishes or the stored context is cancelled. It returns the underlying Result; if the stored context is nil it behaves like WaitWithContext(context.Background()).

func (*Background) WaitWithContext

func (bg *Background) WaitWithContext(ctx context.Context) Result

WaitWithContext blocks until the background command completes or ctx is cancelled. Cancellation returns a Result whose Error is ctx.Err().

type Digest

type Digest any

type PolicyError

type PolicyError struct {
	Verdict Verdict
	Digest  string
}

func (*PolicyError) Error

func (e *PolicyError) Error() string

func (*PolicyError) Is

func (e *PolicyError) Is(target error) bool

type Result

type Result struct {
	ExitCode       int
	Error          error
	CombinedOutput []byte
}

func WaitCommand

func WaitCommand(cmd *exec.Cmd, capture port.CommandCapture) Result

WaitCommand waits for cmd to exit and returns a Result capturing the exit code, error, and any combined output buffered by StartCommand.

type Runnable

type Runnable = port.Runnable

func Open

func Open(executablePayload []byte) (Runnable, error)

Open attempts to create a memory file descriptor using memfd_create(2), name will be a sha256 hash of the payload that will show up under /proc/<pid>/{fd,fdinfo}, running process will show up as /proc/self/fd/<int>. Returns an open file descriptor (or error) where Name() can be used to execute it. Close the fd when done. If unix.MemfdCreate() fails, Open will fall back to writing the payload as a temporary file with user execute bit set. When Close() is called, the temporary file will be deleted. Payload can be anything Linux/Android can execute (ELF and script shebang). Example:

//go:embed myapp
var elfOrShebangScript []byte
//...
f, err := emrun.Open("myapp", elfOrShebangScript)
if err != nil {
	panic(err)
}
defer f.Close()
cmd := exec.Command(f.Name(), "--version")
//...
cmd.Run()

type Verdict

type Verdict int
const (
	ALLOW Verdict = iota
	DENY
)

func (Verdict) String

func (v Verdict) String() string

Directories

Path Synopsis
adapters
The emrun companion package efrun exposes the same API surface as emrun but always executes from a temporary file, which makes it portable to platforms where memfd_create is unavailable, but you have to explicitly choose that import.
The emrun companion package efrun exposes the same API surface as emrun but always executes from a temporary file, which makes it portable to platforms where memfd_create is unavailable, but you have to explicitly choose that import.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL