From 947bd983219edca8dba9db5beb0aba5ad2606fc4 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 19 Oct 2026 11:17:40 +0100 Subject: [PATCH] antd-elixir: move to grpc 1.0.5 (V2-1574), commit mix.lock grpc < 1.0 carries GHSA-grp7-v8xh-rj7h (critical, unsafe term deserialization in the server-side Erlpack codec) plus three high-severity server-side advisories, all fixed in 1.0.0. antd-elixir is client-only and does not reach the vulnerable code, but every install resolved 0.11.5 and Hex flagged it. - grpc ~> 1.0.5; grpc 1.0 split the client from the server, and gun is now an optional dependency of it, so gun ~> 2.4.0 is listed explicitly. grpc_server (test only) supplies the mock servers. - Antd.GrpcAdapter rewritten for grpc 1.0's Gun adapter, which reads a call's response from a per-stream StreamResponseProcess instead of the caller's mailbox. Still needed: 1.0.5 drops headers and trailers on a failed unary call as 0.11.5 did. - Commit mix.lock (un-ignored for antd-elixir only) so CI and dev resolve one tree; the constraints in mix.exs are what protect downstream users. cowlib 2.21.0 / gun (EEF-CVE-2026-43966, medium) has no fixed release and stays flagged by hex.audit; see the ticket. Full suite on dev2: 265 tests, 0 failures, 3 skipped, before and after. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 2 + antd-elixir/lib/antd/grpc_adapter.ex | 110 ++++++++++++++------------- antd-elixir/lib/antd/grpc_client.ex | 2 +- antd-elixir/mix.exs | 11 ++- antd-elixir/mix.lock | 27 +++++++ 5 files changed, 97 insertions(+), 55 deletions(-) create mode 100644 antd-elixir/mix.lock diff --git a/.gitignore b/.gitignore index 8f9a9407..d1dd24c5 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ _build/ deps/ .elixir_ls/ mix.lock +# antd-elixir is a library but commits its lock: CI and dev resolve the same tree +!antd-elixir/mix.lock # Zig .zig-cache/ diff --git a/antd-elixir/lib/antd/grpc_adapter.ex b/antd-elixir/lib/antd/grpc_adapter.ex index f9ded712..41f30932 100644 --- a/antd-elixir/lib/antd/grpc_adapter.ex +++ b/antd-elixir/lib/antd/grpc_adapter.ex @@ -16,20 +16,28 @@ defmodule Antd.GrpcAdapter do # there (`x-antd-error-code`) for the errors that share a gRPC status with # older ones. `GRPC.RPCError` has no field for metadata. # - # How: the call's gun messages are taken from this process's mailbox up to - # the last one of the response stream, their headers and trailers noted, - # and then sent back to this process in their original order for Gun's own - # `receive_data/2` to read, so decoding and every success and error result - # stay Gun's. No message of the stream can arrive after its last one, so - # re-sending them cannot reorder anything. + # How: grpc 1.0's Gun adapter reads a call's response from a per-stream + # `GRPC.Client.Adapters.Gun.StreamResponseProcess`, which queues the + # stream's messages and hands them out one `await/2` at a time. This adapter + # takes the stream's messages out of that process up to the last one of the + # response, notes their headers and trailers, then plays them into a fresh + # `StreamResponseProcess` (as the gun messages it was built to receive) and + # lets Gun's own `receive_data/2` read that one. So decoding and every + # success and error result stay Gun's. # # Calls without `return_headers`, and every server-streaming call, go to # Gun untouched. The SDK only sets `return_headers` on unary calls for its # pointer functions. + # + # Written against grpc 1.0.5 with gun 2.4.1. It leans on + # `StreamResponseProcess.start_link/0`, `await/2` and the gun messages that + # process accepts, and on the `:response_pid` key of the stream payload. + # Re-run test/antd/grpc_pointers_test.exs on any grpc upgrade. @behaviour GRPC.Client.Adapter alias GRPC.Client.Adapters.Gun + alias GRPC.Client.Adapters.Gun.StreamResponseProcess @impl true defdelegate connect(channel, opts), to: Gun @@ -62,15 +70,21 @@ defmodule Antd.GrpcAdapter do def receive_data(stream, opts), do: Gun.receive_data(stream, opts) defp receive_with_metadata(stream, opts) do - %{channel: %{adapter_payload: %{conn_pid: conn_pid}}, payload: %{stream_ref: ref}} = stream + %{payload: %{response_pid: response_pid} = payload} = stream - case collect(conn_pid, ref, await_timeout(opts[:timeout])) do + case collect(response_pid, await_timeout(opts[:timeout]), []) do {:ok, messages} -> - Enum.each(messages, &send(self(), &1)) - - case Gun.receive_data(stream, opts) do - {:error, error} -> {:error, error, metadata(messages)} - result -> result + {:ok, replay} = StreamResponseProcess.start_link() + Enum.each(messages, &send(replay, &1)) + replayed = %{stream | payload: %{payload | response_pid: replay}} + + try do + case Gun.receive_data(replayed, opts) do + {:error, error} -> {:error, error, metadata(messages)} + result -> result + end + after + stop(replay) end {:error, _} = error -> @@ -82,59 +96,49 @@ defmodule Antd.GrpcAdapter do defp await_timeout(timeout) when is_integer(timeout), do: timeout * 2 defp await_timeout(timeout), do: timeout - defp collect(conn_pid, ref, timeout) do - mref = Process.monitor(conn_pid) - - try do - collect(conn_pid, ref, timeout, mref, []) - after - Process.demonitor(mref, [:flush]) - end - end - # Takes the stream's messages until its last one: a response or data frame - # that ends the stream, its trailers, or an error. An error message is kept - # for Gun to read and map as it would have. If the connection goes down, - # what was taken goes back and Gun reports the dead connection. - defp collect(conn_pid, ref, timeout, mref, acc) do - receive do - {:gun_response, ^conn_pid, ^ref, :nofin, _status, _headers} = msg -> - collect(conn_pid, ref, timeout, mref, [msg | acc]) - - {:gun_data, ^conn_pid, ^ref, :nofin, _data} = msg -> - collect(conn_pid, ref, timeout, mref, [msg | acc]) - - {:gun_response, ^conn_pid, ^ref, :fin, _status, _headers} = msg -> - {:ok, Enum.reverse([msg | acc])} - - {:gun_data, ^conn_pid, ^ref, :fin, _data} = msg -> - {:ok, Enum.reverse([msg | acc])} + # that ends the stream, its trailers, or an error. Each comes back as the + # gun message that produced it, for a StreamResponseProcess to queue again. + # A dead stream process answers with a connection error, which Gun reports. + defp collect(response_pid, timeout, acc) do + case StreamResponseProcess.await(response_pid, timeout) do + {:response, fin, status, headers} -> + next(response_pid, timeout, acc, fin, {:gun_response, nil, nil, fin, status, headers}) - {:gun_trailers, ^conn_pid, ^ref, _trailers} = msg -> - {:ok, Enum.reverse([msg | acc])} + {:data, fin, data} -> + next(response_pid, timeout, acc, fin, {:gun_data, nil, nil, fin, data}) - {:gun_inform, ^conn_pid, ^ref, _status, _headers} = msg -> - {:ok, Enum.reverse([msg | acc])} + {:trailers, trailers} -> + {:ok, Enum.reverse([{:gun_trailers, nil, nil, trailers} | acc])} - {:gun_error, ^conn_pid, ^ref, _reason} = msg -> - {:ok, Enum.reverse([msg | acc])} - - {:gun_error, ^conn_pid, _reason} = msg -> - {:ok, Enum.reverse([msg | acc])} - - {:DOWN, ^mref, :process, ^conn_pid, _reason} -> - {:ok, Enum.reverse(acc)} - after - timeout -> + {:error, :timeout} -> # What Gun's adapter answers when the server does not reply in time. {:error, GRPC.RPCError.exception( GRPC.Status.deadline_exceeded(), "timeout when waiting for server" )} + + {:error, {:stream_error, reason}} -> + {:ok, Enum.reverse([{:gun_error, nil, nil, reason} | acc])} + + {:error, {:connection_error, reason}} -> + {:ok, Enum.reverse([{:gun_error, nil, reason} | acc])} + + {:error, {:unexpected_message, _}} = unexpected -> + {:ok, Enum.reverse([unexpected | acc])} end end + defp next(_response_pid, _timeout, acc, :fin, message), do: {:ok, Enum.reverse([message | acc])} + defp next(response_pid, timeout, acc, :nofin, message), do: collect(response_pid, timeout, [message | acc]) + + defp stop(pid) do + GenServer.stop(pid) + catch + :exit, _ -> :ok + end + defp metadata(messages) do Enum.reduce(messages, %{headers: %{}, trailers: %{}}, fn {:gun_response, _, _, _, _, headers}, acc -> diff --git a/antd-elixir/lib/antd/grpc_client.ex b/antd-elixir/lib/antd/grpc_client.ex index a66f7430..cbdcc0a9 100644 --- a/antd-elixir/lib/antd/grpc_client.ex +++ b/antd-elixir/lib/antd/grpc_client.ex @@ -1379,7 +1379,7 @@ defmodule Antd.GrpcClient do # # Message size: a manifest can be 64 MiB, and a Build or Decode response # carries it, so antd-go raises its client's 4 MiB receive default per - # call. grpc-elixir 0.11 has no receive limit to raise: its Gun adapter + # call. grpc-elixir 0.11 and 1.0 have no receive limit to raise: the Gun adapter # reads a unary response whole, whatever its size; GRPC.Stub.connect has no # option for one, and a call rejects any option it does not know; and # sends are capped only by GRPC.Message's 2 GiB default. So no limit is diff --git a/antd-elixir/mix.exs b/antd-elixir/mix.exs index ed958c1d..9c929cbd 100644 --- a/antd-elixir/mix.exs +++ b/antd-elixir/mix.exs @@ -23,8 +23,17 @@ defmodule Antd.MixProject do [ {:req, "~> 0.4"}, {:jason, "~> 1.4"}, - {:grpc, "~> 0.9"}, + # grpc < 1.0 carries the advisories GHSA-grp7-v8xh-rj7h (critical) and + # three more, all fixed in 1.0.0. From 1.0 the client is its own package + # and gun is an optional dependency of it, so it is listed here. + # Antd.GrpcAdapter copies from grpc's Gun adapter internals, so keep the + # range to one minor line and re-run test/antd/grpc_pointers_test.exs + # when moving it. + {:grpc, "~> 1.0.5"}, + {:gun, "~> 2.4.0"}, {:protobuf, "~> 0.12"}, + # Only the test suite's mock servers use the server half. + {:grpc_server, "~> 1.0.5", only: :test}, {:bypass, "~> 2.1", only: :test} ] end diff --git a/antd-elixir/mix.lock b/antd-elixir/mix.lock new file mode 100644 index 00000000..b1538b30 --- /dev/null +++ b/antd-elixir/mix.lock @@ -0,0 +1,27 @@ +%{ + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, + "cowboy": {:hex, :cowboy, "2.20.0", "63f6e3afa7e14201498142a10d00379f5e11761f69e69782807c713627f6bc73", [:make, :rebar3], [{:cowlib, ">= 2.21.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "a0be47464a2887f71c061ce90723dc17441c9b81ba9569cd9b596eb0367d1edd"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.21.0", "3ea757c79784bdbdebac7857ce3adf2290af6b8680d4248c75e40ced2e98c89e", [:make, :rebar3], [], "hexpm", "0960248773762a5319b3c17285028bc76f54f58ef0f9451367401bdf67b46274"}, + "finch": {:hex, :finch, "0.24.0", "4022b6194e907b6d9b597c168001cac38e488a35fe36cef9d8297e3159782510", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "33ba40069c3587c2f99f9125b766e19dad87d6d54be3c6961db2304df04cef00"}, + "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, + "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"}, + "googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"}, + "grpc": {:hex, :grpc, "1.0.5", "71082f1a2d0ff6d2a4198efaf514c33e71485167d1e278022bc0641cd788cea2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:grpc_core, "~> 1.0.5", [hex: :grpc_core, repo: "hexpm", optional: false]}, {:gun, "~> 2.4.0", [hex: :gun, repo: "hexpm", optional: true]}, {:mint, "~> 1.9", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "5e892e542a2e0f05ed17a4ad3814483e3980eaa93906f993dac9e08369b178ea"}, + "grpc_core": {:hex, :grpc_core, "1.0.5", "add39a2ca38604f73c8b2feed30c0fd2d2291daedac8060ee1a75b5658f0a69d", [:mix], [{:googleapis, "~> 0.1.0", [hex: :googleapis, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.17", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a18ae7380cfebc20271aad36393e8aa041e1faa926fa25f40328e4350f83d8f0"}, + "grpc_server": {:hex, :grpc_server, "1.0.5", "7cad10f412de85d5a7329f09302f17644142702ca7d5cbe47cf68a6cce463ad9", [:mix], [{:cowboy, "~> 2.14", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowlib, "~> 2.14", [hex: :cowlib, repo: "hexpm", optional: false]}, {:flow, "~> 1.2", [hex: :flow, repo: "hexpm", optional: false]}, {:grpc_core, "~> 1.0.5", [hex: :grpc_core, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.17", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1a7df870e92dad4d7c12ec1bd264a06c94cbab2f12b054064636ee2b486607b0"}, + "gun": {:hex, :gun, "2.4.1", "c3a8bff8e155e1fc8e499216599bb7effbd27bc1985662a8b25016090dc18428", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "1450575b4d393aa0811322727b90ad1dd94b5c10026f54a1641785bcbd250384"}, + "hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.11.0", "a713551624815c0435237b93d90ea8b9b14254690c66f732d0ef8930f76ff1d9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "c6279ba2d6aa3a383a1d4cfbe7b59f42e6efd400f58d8e2acfeac48a438693ab"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.9.0", "87e21e0d9054ced99c36d128f49e3ea2cd8b745fffb97de50bff99706087af4f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2002bafba4f3a45b55a58e68d70211b153a7ed18d37edb1ceb6e96e7a92c422e"}, + "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, + "protobuf": {:hex, :protobuf, "0.17.1", "5c058a4abbdf3c094bbdab142022b22950dd25214fcfdc624ae9bf0c60f9346f", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "76770ecda0ef057e540932f15b5a143ba8877875e88727f53429a7dc4dbf8b6c"}, + "ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"}, + "req": {:hex, :req, "0.7.5", "0c3293cbf7717aba20b1b1b3ea13fe8d3fdf6f1557490f390c3f1ef6dd9db955", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "576853c4bf403faa3c1fa5d093e0759227083b78f8a6864c67d62c0a9c45514d"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, +}