Skip to content

Commit a401819

Browse files
committed
fix(node): ignore stale port exits in trap_exit device servers
ThinPool, Img.Server, Layer.Server and Img.Mutable each run privileged commands through `System.cmd`, which links a transient port to the caller and returns only once the command has finished. Because these servers trap exits (for `terminate/2` teardown), the now-defunct port's exit is then delivered as `{:EXIT, port, reason}`. None of these servers define a catch-all handle_info, so an unmatched `{:EXIT, ...}` raises a FunctionClauseError and crash-loops the server -- Layer.Server crashed while mounting a layer, which surfaced to create_vm as :no_capacity. Match the port specifically (`is_port/1`) and ignore it regardless of reason: the synchronous `System.cmd` has already consumed the command's result, so the trailing port exit is stale by construction whether it closed normally or not. A linked *process* exiting is a genuinely different event and still falls through (and raises), so real faults are not masked.
1 parent 9ee5de4 commit a401819

4 files changed

Lines changed: 32 additions & 0 deletions

File tree

lib/hyper/node/img/mutable.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ defmodule Hyper.Node.Img.Mutable do
119119
@impl true
120120
def handle_info(:idle_timeout, state), do: {:noreply, state}
121121

122+
@impl true
123+
# Each privileged command runs through `System.cmd`, which links a transient
124+
# port to this process and returns only once that command has finished. Because
125+
# we trap exits (for `terminate/2` teardown), the now-defunct port's exit lands
126+
# here afterwards -- stale by construction, whatever its reason -- so ignore any
127+
# port exit. (A linked *process* exiting is a different event and still raises.)
128+
def handle_info({:EXIT, port, _reason}, state) when is_port(port), do: {:noreply, state}
129+
122130
@impl true
123131
def terminate(_reason, state) do
124132
# Destroy the thin volume, then release the image (its monitor on us also

lib/hyper/node/img/server.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ defmodule Hyper.Node.Img.Server do
128128
{:noreply, state}
129129
end
130130

131+
@impl true
132+
# Each privileged command runs through `System.cmd`, which links a transient
133+
# port to this process and returns only once that command has finished. Because
134+
# we trap exits (for `terminate/2` teardown), the now-defunct port's exit lands
135+
# here afterwards -- stale by construction, whatever its reason -- so ignore any
136+
# port exit. (A linked *process* exiting is a different event and still raises.)
137+
def handle_info({:EXIT, port, _reason}, state) when is_port(port), do: {:noreply, state}
138+
131139
@impl true
132140
def terminate(_reason, %State{dm_names: dm_names}) do
133141
# Remove top-down (a snapshot's origin is the device below it). Layers are

lib/hyper/node/img/thin_pool.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ defmodule Hyper.Node.Img.ThinPool do
9494
{:reply, :ok, id_free(state, id)}
9595
end
9696

97+
@impl true
98+
# Each privileged command runs through `System.cmd`, which links a transient
99+
# port to this process and returns only once that command has finished. Because
100+
# we trap exits (for `terminate/2` teardown), the now-defunct port's exit lands
101+
# here afterwards -- stale by construction, whatever its reason -- so ignore any
102+
# port exit. (A linked *process* exiting is a different event and still raises.)
103+
def handle_info({:EXIT, port, _reason}, state) when is_port(port), do: {:noreply, state}
104+
97105
@impl true
98106
def terminate(_reason, state) do
99107
_ = SuidHelper.Dmsetup.remove(@pool_name)

lib/hyper/node/layer/server.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ defmodule Hyper.Node.Layer.Server do
119119
{:noreply, state}
120120
end
121121

122+
@impl true
123+
# Each privileged command runs through `System.cmd`, which links a transient
124+
# port to this process and returns only once that command has finished. Because
125+
# we trap exits (for `terminate/2` teardown), the now-defunct port's exit lands
126+
# here afterwards -- stale by construction, whatever its reason -- so ignore any
127+
# port exit. (A linked *process* exiting is a different event and still raises.)
128+
def handle_info({:EXIT, port, _reason}, state) when is_port(port), do: {:noreply, state}
129+
122130
@impl true
123131
def terminate(_reason, %State{blk_path: blk_path}) do
124132
case SuidHelper.Losetup.detach(blk_path) do

0 commit comments

Comments
 (0)