Class: Hermetic::Backends::Docker

Inherits:
Base
  • Object
show all
Defined in:
lib/hermetic/backends/docker.rb

Overview

Hardened docker run — Silas's interim adapter ported onto the Request contract and extended with what it lacked (files:, stdin:, clean env, per-run limits). Container namespaces are weaker than a microVM and the daemon this adapter shells to lives on the app host, so this is trust :host — dev-only for untrusted code (spec §3).

The argv is a pure function of the Request — files: included — so the whole wire form is unit-testable without Docker present; the runner is injectable and a real run is a single docker run leg.

Direct Known Subclasses

Gvisor

Constant Summary

Constants inherited from Base

Base::OFF_HOST_TRUST, Base::TIMEOUT_EXIT

Instance Attribute Summary collapse

Attributes inherited from Base

#limits, #trust

Instance Method Summary collapse

Methods inherited from Base

#backend_name, #enabled?, #off_host?, #run

Methods included from SilasLedgerGuard

#run

Constructor Details

#initialize(image:, workdir: "/workspace", docker_bin: "docker", runner: nil, **base_opts) ⇒ Docker

Returns a new instance of Docker.

Raises:



18
19
20
21
22
23
24
25
26
# File 'lib/hermetic/backends/docker.rb', line 18

def initialize(image:, workdir: "/workspace", docker_bin: "docker", runner: nil, **base_opts)
  raise ConfigError, "image: is required" if image.to_s.strip.empty?

  super(trust: :host, **base_opts)
  @image = image
  @workdir = workdir
  @docker_bin = docker_bin
  @runner = runner || method(:shell_run)
end

Instance Attribute Details

#imageObject (readonly)

Returns the value of attribute image.



16
17
18
# File 'lib/hermetic/backends/docker.rb', line 16

def image
  @image
end

#workdirObject (readonly)

Returns the value of attribute workdir.



16
17
18
# File 'lib/hermetic/backends/docker.rb', line 16

def workdir
  @workdir
end

Instance Method Details

#bootstrap_script(request) ⇒ Object

Pure: the files: delivery. docker cp can't reach a read-only rootfs and a workdir tmpfs only exists once the container starts (the smoke layer proved both), so files are written by the guest itself at exec time: each one a base64 blob decoded into the workdir tmpfs, then exec "$@" hands off to the real command. User data enters only as base64 ([A-Za-z0-9+/=], inert in single quotes) and escaped paths; the command stays positional-args — still no injection surface. Needs /bin/sh and base64 in the image (any debian/alpine/busybox).



54
55
56
57
58
59
60
61
# File 'lib/hermetic/backends/docker.rb', line 54

def bootstrap_script(request)
  writes = request.files.map do |path, content|
    dir = File.dirname(path)
    write = "printf '%s' '#{[ content ].pack("m0")}' | base64 -d > #{Shellwords.escape(path)}"
    dir == "." ? write : "mkdir -p #{Shellwords.escape(dir)} && #{write}"
  end
  (writes + [ 'exec "$@"' ]).join(" && ")
end

#container_argv(request, name:) ⇒ Object

Pure: Request -> locked-down docker run argv. The command rides as argv elements, never interpolated into a shell string — no injection surface. Docker passes no host vars unless named in --env, so the guest env is exactly Request#clean_env by construction. The rootfs is read-only; the workdir is a tmpfs — the guest's only writable scratch, which is also where the files: bootstrap lands.



34
35
36
37
38
39
40
41
42
43
44
# File 'lib/hermetic/backends/docker.rb', line 34

def container_argv(request, name:)
  [ @docker_bin, "run", "--rm", "--name", name, *runtime_flags,
    *stdin_flags(request),
    "--network", request.network.docker_flag,
    "--memory", request.limits.memory, "--cpus", request.limits.cpus,
    "--pids-limit", request.limits.pids.to_s,
    "--read-only", "--tmpfs", @workdir, "--workdir", @workdir,
    "--cap-drop", "ALL", "--security-opt", "no-new-privileges",
    *env_flags(request),
    @image, *guest_argv(request) ]
end