Skip to content

Commit 26b7700

Browse files
committed
feat(cfg): Hyper.Cfg.Tools facade; define missing privileged tool paths
1 parent c3be051 commit 26b7700

8 files changed

Lines changed: 132 additions & 19 deletions

File tree

lib/hyper/cfg/tools.ex

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
defmodule Hyper.Cfg.Tools do
2+
@moduledoc """
3+
Paths to every external binary Hyper runs, read from the `[tools]` table.
4+
5+
The privileged tools (`firecracker`, `jailer`, `dmsetup`, `losetup`,
6+
`blockdev`) are read **only** from `/etc/hyper/config.toml` — the file the
7+
setuid helper also parses, so node and helper can never disagree on a
8+
root-impacting path. The node-only tools (`skopeo`, `mke2fs`, `umoci`,
9+
`suidhelper`) run unprivileged, so `/etc/hyper/config.exs` may override them
10+
(e.g. a path from a secrets manager), then `config.toml`, then the default.
11+
"""
12+
13+
import Hyper.Cfg, only: [get_cfg: 1]
14+
15+
@doc "Firecracker binary. Required — raises if `[tools] firecracker` is unset."
16+
@spec firecracker :: Path.t()
17+
def firecracker, do: get_cfg(toml: "tools.firecracker")
18+
19+
@doc "Non-raising `firecracker/0`."
20+
@spec firecracker_configured :: {:ok, Path.t()} | :error
21+
def firecracker_configured, do: Hyper.Cfg.Toml.fetch("tools.firecracker")
22+
23+
@doc "Jailer binary. Required — raises if `[tools] jailer` is unset."
24+
@spec jailer :: Path.t()
25+
def jailer, do: get_cfg(toml: "tools.jailer")
26+
27+
@doc "Non-raising `jailer/0`."
28+
@spec jailer_configured :: {:ok, Path.t()} | :error
29+
def jailer_configured, do: Hyper.Cfg.Toml.fetch("tools.jailer")
30+
31+
@doc "dmsetup binary (privileged, config.toml-only)."
32+
@spec dmsetup :: Path.t()
33+
def dmsetup, do: get_cfg(toml: "tools.dmsetup", default: "/usr/sbin/dmsetup")
34+
35+
@doc "losetup binary (privileged, config.toml-only)."
36+
@spec losetup :: Path.t()
37+
def losetup, do: get_cfg(toml: "tools.losetup", default: "/usr/sbin/losetup")
38+
39+
@doc "blockdev binary (privileged, config.toml-only)."
40+
@spec blockdev :: Path.t()
41+
def blockdev, do: get_cfg(toml: "tools.blockdev", default: "/usr/sbin/blockdev")
42+
43+
@doc "skopeo binary (node tool). config.exs > config.toml > `skopeo` on PATH."
44+
@spec skopeo :: String.t()
45+
def skopeo, do: get_cfg(runtime: {__MODULE__, :skopeo}, toml: "tools.skopeo", default: "skopeo")
46+
47+
@doc "mke2fs binary (node tool). config.exs > config.toml > `mke2fs` on PATH."
48+
@spec mke2fs :: String.t()
49+
def mke2fs, do: get_cfg(runtime: {__MODULE__, :mke2fs}, toml: "tools.mke2fs", default: "mke2fs")
50+
51+
@doc "umoci binary (node tool), or `nil` to let Hyper download a pinned release."
52+
@spec umoci :: String.t() | nil
53+
def umoci, do: get_cfg(runtime: {__MODULE__, :umoci}, toml: "tools.umoci", default: nil)
54+
55+
@doc "setuid device helper (node tool). config.exs > config.toml > install default."
56+
@spec suidhelper :: String.t()
57+
def suidhelper,
58+
do:
59+
get_cfg(
60+
runtime: {__MODULE__, :suidhelper},
61+
toml: "tools.suidhelper",
62+
default: "/usr/local/bin/hyper-suidhelper"
63+
)
64+
end

lib/hyper/img/oci_loader.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ defmodule Hyper.Img.OciLoader do
8181
def test_system do
8282
with {:ok, _arch} <- Sys.Arch.current() do
8383
tools = [
84-
{"skopeo", Config.skopeo_path()},
84+
{"skopeo", Hyper.Cfg.Tools.skopeo()},
8585
{"umoci", Umoci.bin()},
86-
{"mke2fs", Config.mke2fs_path()}
86+
{"mke2fs", Hyper.Cfg.Tools.mke2fs()}
8787
]
8888

8989
missing = for {name, path} <- tools, System.find_executable(path) == nil, do: name
@@ -137,7 +137,7 @@ defmodule Hyper.Img.OciLoader do
137137
bundle = Path.join(tmp, "bundle")
138138

139139
skopeo =
140-
cmd(Config.skopeo_path(), [
140+
cmd(Hyper.Cfg.Tools.skopeo(), [
141141
"copy",
142142
"--override-os",
143143
"linux",
@@ -192,7 +192,7 @@ defmodule Hyper.Img.OciLoader do
192192
["-t", "ext4", "-F", "-q", "-N", to_string(inodes), "-d", rootfs, staged] ++
193193
["#{Information.as_mib(size)}M"]
194194

195-
case tag(cmd(Config.mke2fs_path(), args), :mke2fs) do
195+
case tag(cmd(Hyper.Cfg.Tools.mke2fs(), args), :mke2fs) do
196196
:ok ->
197197
{:ok, staged}
198198

lib/hyper/img/oci_loader/umoci.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ defmodule Hyper.Img.OciLoader.Umoci do
4040
"""
4141
@spec ensure_installed() :: :ok | {:error, term()}
4242
def ensure_installed do
43-
if Config.umoci_path() != nil do
43+
if Hyper.Cfg.Tools.umoci() != nil do
4444
:ok
4545
else
4646
with {:ok, arch} <- Sys.Arch.current() do
@@ -57,7 +57,7 @@ defmodule Hyper.Img.OciLoader.Umoci do
5757
"""
5858
@spec bin() :: Path.t()
5959
def bin do
60-
case Config.umoci_path() do
60+
case Hyper.Cfg.Tools.umoci() do
6161
nil ->
6262
{:ok, arch} = Sys.Arch.current()
6363
default_path(arch)

lib/hyper/suid_helper.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ defmodule Hyper.SuidHelper do
3030
@spec exec([String.t()]) :: {:ok, map()} | {:error, err()}
3131
@decorate with_span("Hyper.SuidHelper.exec", include: [:argv])
3232
def exec(argv) do
33-
case System.cmd(Hyper.Config.suid_helper(), argv, stderr_to_stdout: true) do
33+
case System.cmd(Hyper.Cfg.Tools.suidhelper(), argv, stderr_to_stdout: true) do
3434
{out, 0} -> {:ok, Jason.decode!(out)}
3535
{out, code} -> {:error, {code, String.trim(out)}}
3636
end
@@ -91,7 +91,7 @@ defmodule Hyper.SuidHelper do
9191

9292
@spec helper_present() :: :ok | {:error, :suid_helper_not_found}
9393
defp helper_present do
94-
if System.find_executable(Hyper.Config.suid_helper()),
94+
if System.find_executable(Hyper.Cfg.Tools.suidhelper()),
9595
do: :ok,
9696
else: {:error, :suid_helper_not_found}
9797
end

lib/hyper/suid_helper/blockdev.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule Hyper.SuidHelper.Blockdev do
1111
@spec device_sectors(Path.t()) :: {:ok, pos_integer()} | {:error, err()}
1212
@decorate with_span("Hyper.SuidHelper.Blockdev.device_sectors", include: [:path])
1313
def device_sectors(path) do
14-
case SuidHelper.exec(["blockdev", "--bin", Hyper.Config.blockdev_path(), "--getsz", path]) do
14+
case SuidHelper.exec(["blockdev", "--bin", Hyper.Cfg.Tools.blockdev(), "--getsz", path]) do
1515
{:ok, %{"sectors" => n}} -> {:ok, n}
1616
{:error, _} = err -> err
1717
end
@@ -21,7 +21,7 @@ defmodule Hyper.SuidHelper.Blockdev do
2121
@spec test_system() :: :ok | {:error, :blockdev_not_found}
2222
@decorate with_span("Hyper.SuidHelper.Blockdev.test_system")
2323
def test_system do
24-
if System.find_executable(Hyper.Config.blockdev_path()),
24+
if System.find_executable(Hyper.Cfg.Tools.blockdev()),
2525
do: :ok,
2626
else: {:error, :blockdev_not_found}
2727
end

lib/hyper/suid_helper/dmsetup.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ defmodule Hyper.SuidHelper.Dmsetup do
6262
case SuidHelper.exec([
6363
"dmsetup",
6464
"--bin",
65-
Hyper.Config.dmsetup_path(),
65+
Hyper.Cfg.Tools.dmsetup(),
6666
"remove",
6767
"--retry",
6868
name
@@ -77,7 +77,7 @@ defmodule Hyper.SuidHelper.Dmsetup do
7777
@decorate with_span("Hyper.SuidHelper.Dmsetup.message", include: [:name, :message])
7878
def message(name, message) do
7979
argv =
80-
["dmsetup", "--bin", Hyper.Config.dmsetup_path(), "message", name, "--message", message]
80+
["dmsetup", "--bin", Hyper.Cfg.Tools.dmsetup(), "message", name, "--message", message]
8181

8282
case SuidHelper.exec(argv) do
8383
{:ok, _} -> :ok
@@ -92,7 +92,7 @@ defmodule Hyper.SuidHelper.Dmsetup do
9292
@spec test_system() :: :ok | {:error, term()}
9393
@decorate with_span("Hyper.SuidHelper.Dmsetup.test_system")
9494
def test_system do
95-
if System.find_executable(Hyper.Config.dmsetup_path()),
95+
if System.find_executable(Hyper.Cfg.Tools.dmsetup()),
9696
do: test_targets(),
9797
else: {:error, :dmsetup_not_found}
9898
end
@@ -101,7 +101,7 @@ defmodule Hyper.SuidHelper.Dmsetup do
101101
@spec test_targets() :: :ok | {:error, term()}
102102
@decorate with_span("Hyper.SuidHelper.Dmsetup.test_targets")
103103
def test_targets do
104-
case System.cmd(Hyper.Config.dmsetup_path(), ["targets"], stderr_to_stdout: true) do
104+
case System.cmd(Hyper.Cfg.Tools.dmsetup(), ["targets"], stderr_to_stdout: true) do
105105
{out, 0} ->
106106
have = parse_targets(out)
107107
missing = Enum.reject(@required_targets, &MapSet.member?(have, &1))
@@ -146,7 +146,7 @@ defmodule Hyper.SuidHelper.Dmsetup do
146146
@spec create(String.t(), String.t(), [String.t()]) :: {:ok, Path.t()} | {:error, err()}
147147
defp create(name, table, flags) do
148148
argv =
149-
["dmsetup", "--bin", Hyper.Config.dmsetup_path(), "create", name] ++
149+
["dmsetup", "--bin", Hyper.Cfg.Tools.dmsetup(), "create", name] ++
150150
flags ++ ["--table", table]
151151

152152
case SuidHelper.exec(argv) do

lib/hyper/suid_helper/losetup.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule Hyper.SuidHelper.Losetup do
1111
@spec attach_ro(Path.t()) :: {:ok, Path.t()} | {:error, err()}
1212
@decorate with_span("Hyper.SuidHelper.Losetup.attach_ro", include: [:path])
1313
def attach_ro(path) do
14-
case SuidHelper.exec(["losetup", "--bin", Hyper.Config.losetup_path(), "attach", path]) do
14+
case SuidHelper.exec(["losetup", "--bin", Hyper.Cfg.Tools.losetup(), "attach", path]) do
1515
{:ok, %{"device" => dev}} -> {:ok, dev}
1616
{:error, _} = err -> err
1717
end
@@ -24,7 +24,7 @@ defmodule Hyper.SuidHelper.Losetup do
2424
case SuidHelper.exec([
2525
"losetup",
2626
"--bin",
27-
Hyper.Config.losetup_path(),
27+
Hyper.Cfg.Tools.losetup(),
2828
"attach",
2929
"--rw",
3030
path
@@ -38,7 +38,7 @@ defmodule Hyper.SuidHelper.Losetup do
3838
@spec detach(Path.t()) :: :ok | {:error, err()}
3939
@decorate with_span("Hyper.SuidHelper.Losetup.detach", include: [:dev])
4040
def detach(dev) do
41-
case SuidHelper.exec(["losetup", "--bin", Hyper.Config.losetup_path(), "detach", dev]) do
41+
case SuidHelper.exec(["losetup", "--bin", Hyper.Cfg.Tools.losetup(), "detach", dev]) do
4242
{:ok, _} -> :ok
4343
{:error, _} = err -> err
4444
end
@@ -48,7 +48,7 @@ defmodule Hyper.SuidHelper.Losetup do
4848
@spec test_system() :: :ok | {:error, :losetup_not_found}
4949
@decorate with_span("Hyper.SuidHelper.Losetup.test_system")
5050
def test_system do
51-
if System.find_executable(Hyper.Config.losetup_path()),
51+
if System.find_executable(Hyper.Cfg.Tools.losetup()),
5252
do: :ok,
5353
else: {:error, :losetup_not_found}
5454
end

test/hyper/cfg/tools_test.exs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
defmodule Hyper.Cfg.ToolsTest do
2+
use ExUnit.Case, async: false
3+
4+
alias Hyper.Cfg.Tools
5+
alias Hyper.Cfg.Toml
6+
7+
setup do
8+
# Hermetic: empty TOML cache so we assert built-in defaults, not the
9+
# ambient /etc/hyper/config.toml. Restore the real cache afterward.
10+
Toml.put_cache(%{})
11+
on_exit(fn -> Toml.reload() end)
12+
:ok
13+
end
14+
15+
test "privileged tools with no config.toml fall back to their sbin defaults" do
16+
assert Tools.dmsetup() == "/usr/sbin/dmsetup"
17+
assert Tools.losetup() == "/usr/sbin/losetup"
18+
assert Tools.blockdev() == "/usr/sbin/blockdev"
19+
end
20+
21+
test "privileged tool paths come only from the [tools] table" do
22+
Toml.put_cache(%{"tools" => %{"dmsetup" => "/custom/dmsetup"}})
23+
assert Tools.dmsetup() == "/custom/dmsetup"
24+
end
25+
26+
test "node tools default to bare PATH names / known install path" do
27+
assert Tools.skopeo() == "skopeo"
28+
assert Tools.mke2fs() == "mke2fs"
29+
assert Tools.suidhelper() == "/usr/local/bin/hyper-suidhelper"
30+
assert Tools.umoci() == nil
31+
end
32+
33+
test "node tools: config.exs (runtime) overrides config.toml" do
34+
Toml.put_cache(%{"tools" => %{"skopeo" => "/from/toml"}})
35+
assert Tools.skopeo() == "/from/toml"
36+
37+
Application.put_env(:hyper, Tools, skopeo: "/from/exs")
38+
assert Tools.skopeo() == "/from/exs"
39+
after
40+
Application.delete_env(:hyper, Tools)
41+
end
42+
43+
test "required tools raise (non-raising form returns :error) when unset" do
44+
assert Tools.firecracker_configured() == :error
45+
assert Tools.jailer_configured() == :error
46+
assert_raise Hyper.Cfg.MissingError, fn -> Tools.firecracker() end
47+
assert_raise Hyper.Cfg.MissingError, fn -> Tools.jailer() end
48+
end
49+
end

0 commit comments

Comments
 (0)