diff --git a/.audit_ignore b/.audit_ignore new file mode 100644 index 00000000..8fea9587 --- /dev/null +++ b/.audit_ignore @@ -0,0 +1,72 @@ +# Dependency-advisory ignores for `mix quality.check`. +# +# Format: one advisory id per line; `#` lines are comments. The gate reads this +# file twice: `mix deps.audit --ignore-file .audit_ignore` (mix_audit) reads it +# directly, and `mix hex.audit` reads it through audit_ignored_advisory_ids/0 in +# mix.exs. hex.audit stays in the gate because mix_audit does not check for +# retired packages; with these ids ignored it still fails on a retirement or on +# any advisory not listed here. +# +# Every entry records what was verified, and the condition that retires it. +# Verified 2026-09-15 against the ERLEF CNA records that hex.audit serves +# (https://api.osv.dev/v1/vulns/), the GitHub Advisory Database +# (`gh api /advisories?cve_id=`), and the cowlib and cowboy sources at the +# versions mix.lock pins. +# +# Shared facts. mix.lock pins cowlib 2.20.0, the newest release on Hex, and +# cowboy 2.19.0. Both arrive only through ex_mcp, which imp declares +# `runtime: false`; imp's own Plug.Cowboy call sites are tests, bench and the +# source-checkout-only demo task, none of which ship in the Hex package. All +# three ERLEF records carry an open-ended Hex range (introduced 2.9.0, no fixed +# version), so hex.audit flags every cowlib release that exists, including the +# ones that carry the fix. That is why the advisory gate moved to mix_audit. + +# EEF-CVE-2026-43971 / CVE-2026-43971 (MEDIUM) - Link header directive smuggling +# via unescaped target/rel/attribute keys in cow_link:link/1. +# VERIFIED FIXED in the version we ship. The ERLEF record names fix commit +# 89da27ee4c241f5d649ba7d9b7f2188918af6cea ("Link: escape and validate values +# when building", 2026-08-18); in ninenines/cowlib, +# `git merge-base --is-ancestor 89da27ee 2.20.0` returns true, and the record's +# own GIT range ends at that commit and lists every affected version up to +# 2.19.0 but not 2.20.0. Only the open-ended Hex range still matches. +# GHSA-gg23-fwhr-prjh carries no affected package ranges at all. +# RETIRE when the ERLEF Hex range gains `fixed: 2.20.0`. +EEF-CVE-2026-43971 + +# EEF-CVE-2026-43966 / CVE-2026-43966 / GHSA-w4f7-4cxr-rv3c (MEDIUM) - HTTP +# response splitting via non-VCHAR bytes in cow_http_struct_hd:escape_string/2. +# NOT FIXED IN COWLIB: at tag 2.20.0 escape_string/2 still copies every other +# byte through verbatim (`escape_string(<>, Acc) -> escape_string(R, +# <>)`), escaping only \ and ", and the ERLEF GIT range has no +# fixed event. The fix shipped one layer up, in cowboy: GitHub records cowboy +# < 2.16.0 vulnerable, first patched 2.16.0, from commit f77cb9b5 "Add +# invalid_response_headers HTTP/1 option". We pin cowboy 2.19.0, and +# deps/cowboy/src/cowboy_http.erl defaults invalid_response_headers to +# `error_terminate`, which refuses any response header value containing CR or +# LF before it reaches the socket. Plug raises Plug.Conn.InvalidHeaderError on +# the same bytes a layer above that. test/dependency_advisory_mitigation_test.exs +# locks the cowboy floor, the Plug rejection, and that no :imp module calls +# cow_http_struct_hd. +# RETIRE when the ERLEF cowlib record gains a fixed range. If the lock ever +# drops below cowboy 2.16.0 the mitigation test fails first. +EEF-CVE-2026-43966 + +# EEF-CVE-2026-43969 / CVE-2026-43969 / GHSA-g2wm-735q-3f56 (LOW) - Cookie +# request-header injection via an unvalidated encoder in cow_cookie:cookie/1. +# NOT FIXED IN COWLIB AND NO FIXED RELEASE EXISTS. cookie/1 at tag 2.20.0 is +# still the unvalidated five-clause function, the ERLEF GIT range has no fixed +# event, and the only patch that exists anywhere is erlef/cowlib commit +# 177953dd "Preliminary patch for CVE-2026-43969" on a fork that upstream has +# not merged. GitHub's record disagrees with the source: it bounds the +# vulnerable range at cowlib <= 2.16.1 with no patched version, which is why +# mix_audit does not flag our 2.20.0. +# Reachability: cookie/1 builds an outgoing Cookie *request* header, so its +# callers are HTTP clients such as gun, which is not in this dependency tree. +# Nothing in deps/ outside cowlib itself calls it; imp speaks HTTP through +# Req/Finch/Mint; test/dependency_advisory_mitigation_test.exs asserts no :imp +# module imports cow_cookie:cookie/1. +# This one is ignored because no upgrade can remove it, not because it is a +# false positive. It is reported in PR #107 rather than buried here. +# RETIRE the moment cowlib publishes a release that validates cookie/1, and +# move the lock to that release. +EEF-CVE-2026-43969 diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 362baacc..ee929149 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -15,7 +15,6 @@ # exsss export shape; the construction is intentional and correct. [ # defensive clause for non-covered result shapes - {"bench/imp/benchmark_truth/failure_campaign.ex", :pattern_match_cov, {169, 13}}, # MapSet opacity on the stopword set (this one renders only under # --format raw; the default formatter drops it, the count still sees it) {"bench/imp/benchmark_truth/hover_bm25.ex", :call_without_opaque, {191, 36}}, @@ -232,5 +231,6 @@ {"lib/imp/optimizer/artifact.ex", :call_without_opaque, {909, 53}}, {"lib/imp/optimizer/playbook.ex", :pattern_match_cov, {1014, 8}}, {"lib/imp/optimizer/report.ex", :call_without_opaque, {755, 55}}, - {"lib/imp/schema.ex", :pattern_match_cov, {459, 8}} + {"lib/imp/schema.ex", :pattern_match_cov, {459, 8}}, + {"bench/imp/benchmark_truth/failure_campaign.ex", :pattern_match_cov, {168, 13}} ] diff --git a/bench/imp/benchmark_truth/failure_campaign.ex b/bench/imp/benchmark_truth/failure_campaign.ex index fe819dd6..6e72b2bc 100644 --- a/bench/imp/benchmark_truth/failure_campaign.ex +++ b/bench/imp/benchmark_truth/failure_campaign.ex @@ -42,19 +42,15 @@ defmodule Imp.BenchmarkTruth.FailureCampaign do validate_positive!(:iterations, iterations) validate_positive!(:max_concurrency, max_concurrency) validate_positive!(:iteration_timeout_ms, iteration_timeout_ms) - warmup = prepare_runtime(opts) + warmup = prepare_runtime(opts, max_concurrency, iteration_timeout_ms) baseline = runtime_snapshot() telemetry = start_telemetry_capture() cases = - Enum.map(@deterministic_lanes, fn - {id, :concurrency} -> - repeat(id, iterations, iteration_timeout_ms, fn -> - concurrency_iteration(max_concurrency) - end) - - {id, handler} -> - repeat(id, iterations, iteration_timeout_ms, fn -> run_lane(handler) end) + Enum.map(@deterministic_lanes, fn {id, handler} -> + repeat(id, iterations, iteration_timeout_ms, fn -> + lane_iteration(handler, max_concurrency) + end) end) live_cases = run_live_cases(opts) @@ -124,6 +120,9 @@ defmodule Imp.BenchmarkTruth.FailureCampaign do } end + defp lane_iteration(:concurrency, max_concurrency), do: concurrency_iteration(max_concurrency) + defp lane_iteration(handler, _max_concurrency), do: run_lane(handler) + defp run_lane(:cancellation), do: cancellation_iteration() defp run_lane(:timeout), do: timeout_iteration() defp run_lane(:partial_stream), do: partial_stream_iteration() @@ -827,32 +826,46 @@ defmodule Imp.BenchmarkTruth.FailureCampaign do end end - defp prepare_runtime(opts) do + defp prepare_runtime(opts, max_concurrency, iteration_timeout_ms) do # The MCP lane explicitly consumes the optional protocol application. Start # its shared supervisors before measuring per-operation resource leaks; # ordinary Imp startup intentionally does not start ExMCP. {:ok, _} = Application.ensure_all_started(:ex_mcp) + warmed_lanes = warm_deterministic_lanes(max_concurrency, iteration_timeout_ms) + + warmup = %{ + "performed" => true, + "warmed_lanes" => warmed_lanes, + "network_hosts" => [], + "external_network" => false, + "billable_generation" => false + } if Keyword.get(opts, :live, false) do - %{ - "performed" => true, + Map.merge(warmup, %{ "authority" => "local_injected_transport", - "network_hosts" => [], - "external_network" => false, - "billable_generation" => false, "dummy_canary_sha256" => sha256(@dummy_canary) - } + }) else - %{ - "performed" => true, - "authority" => "local_protocol_runtime", - "network_hosts" => [], - "external_network" => false, - "billable_generation" => false - } + Map.put(warmup, "authority", "local_protocol_runtime") end end + # Run every deterministic lane once before the leak baseline is taken. The + # first real HTTP round trip in a VM lazily starts services that then live for + # the rest of the VM: Plug.Cowboy attaches its :plug_cowboy telemetry handler + # on first use, and the kernel starts inet_gethost_native (two processes and + # one port) on the first hostname lookup. Neither is a per-iteration leak, but + # a cold baseline counted both as leaks whenever this campaign happened to be + # the VM's first HTTP user, which depended on the test seed. Warmup results + # are discarded; the measured iterations report any lane failure. + defp warm_deterministic_lanes(max_concurrency, iteration_timeout_ms) do + Enum.map(@deterministic_lanes, fn {id, handler} -> + _ = normalize(fn -> lane_iteration(handler, max_concurrency) end, 0, iteration_timeout_ms) + id + end) + end + defp repeat_live(id, iterations, timeout_ms, fun) do started_at = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() baseline = runtime_snapshot() diff --git a/benchmarks/requirements-ifbench-parity.txt b/benchmarks/requirements-ifbench-parity.txt index b8ce8d0e..aa76549d 100644 --- a/benchmarks/requirements-ifbench-parity.txt +++ b/benchmarks/requirements-ifbench-parity.txt @@ -1,7 +1,7 @@ emoji==2.15.0 immutabledict==4.3.1 langdetect==1.0.9 -nltk==3.10.0 +nltk==3.10.3 packaging==26.2 setuptools==83.0.0 syllapy==0.7.2 diff --git a/mix.exs b/mix.exs index a671d571..11c96f76 100644 --- a/mix.exs +++ b/mix.exs @@ -23,6 +23,7 @@ defmodule Imp.MixProject do skip_code_autolink_to: &skip_filtered_doc_reference?/1 ], start_permanent: Mix.env() == :prod, + hex: [ignore_advisories: audit_ignored_advisory_ids()], elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), aliases: aliases(), @@ -120,7 +121,8 @@ defmodule Imp.MixProject do {:stream_data, "~> 1.1", only: :test}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, - {:ex_doc, "~> 0.35", only: [:dev, :test], runtime: false} + {:ex_doc, "~> 0.35", only: [:dev, :test], runtime: false}, + {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false} ] end @@ -404,8 +406,18 @@ defmodule Imp.MixProject do "dialyzer.check": [ "dialyzer" ], + # Dependency advisories run through mix_audit, whose database records a + # fixed range per advisory. mix hex.audit serves the ERLEF feed, where the + # three open cowlib records are open-ended (introduced 2.9.0, no fixed + # version), so it flags every cowlib release that exists including the one + # carrying the fix. hex.audit stays in the gate for retired packages, + # which mix_audit does not check, with those ids ignored from + # .audit_ignore -- one file holding each id next to what was verified and + # what retires it. Both run as child invocations: mix deps.audit stops the + # VM when it finds something. "quality.check": [ "credo --only warning", + "cmd mix deps.audit --ignore-file .audit_ignore", "cmd mix hex.audit" ] ] @@ -509,6 +521,24 @@ defmodule Imp.MixProject do ] end + # Advisory ids the quality gate accepts, read from .audit_ignore so the ids, + # the reason each was verified to be safe to ignore, and the condition that + # retires it live in one file that both audit steps read. Absent in a Hex + # package checkout, where the gates do not run. + defp audit_ignored_advisory_ids do + path = Path.expand(".audit_ignore", __DIR__) + + if File.regular?(path) do + path + |> File.read!() + |> String.split("\n") + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == "" or String.starts_with?(&1, "#"))) + else + [] + end + end + defp benchmark_tasks_available? do File.exists?("lib/mix/tasks/imp.benchmark.run.ex") end diff --git a/mix.lock b/mix.lock index 19e13be9..daf4bfa1 100644 --- a/mix.lock +++ b/mix.lock @@ -34,6 +34,7 @@ "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.10.0", "85af3353bfc504f5bdfe494bd92b8490f87a306dc659ee1ad0af435107e898dc", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "8b16fb72aaa7531d206a1f05e4cc85509ba531ccec7a17a22736c9c95cbb24d1"}, "mint_web_socket": {:hex, :mint_web_socket, "1.0.6", "5ffcf350df5b90f2d7a04adf877165228804993714592512374218d4679e325a", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "0c360e9012413f1c115a63532601eb5d63731aab7010949178769760686c1698"}, + "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.2", "fa8a6f2d8c592ad4d79b2ca617473c6aefd5869abfa02563a77682038bf916cf", [:mix], [], "hexpm", "098af64e1f6f8609c6672127cfe9e9590a5d3fcdd82bc17a377b8692fd81a879"}, @@ -55,5 +56,7 @@ "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websockex": {:hex, :websockex, "0.5.1", "9de28d37bbe34f371eb46e29b79c94c94fff79f93c960d842fbf447253558eb4", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8ef39576ed56bc3804c9cd8626f8b5d6b5721848d2726c0ccd4f05385a3c9f14"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, "zoi": {:hex, :zoi, "0.18.5", "fc23f53d5ba6c8639fb30d9a70a80ae8fed774c180c2544bfd16f53bedf95dff", [:mix], [{:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "d69c2e6468752367941d0264e21cf229c1ba8757b2973eea17c12b3cb7d2d4c4"}, } diff --git a/priv/trl_worker/pyproject.toml b/priv/trl_worker/pyproject.toml index ff166776..01a81139 100644 --- a/priv/trl_worker/pyproject.toml +++ b/priv/trl_worker/pyproject.toml @@ -6,7 +6,7 @@ dependencies = [ "datasets==5.0.1", "peft==0.18.1", "torch==2.13.0", - "transformers==5.5.0", + "transformers==5.10.1", "trl==1.6.0", ] diff --git a/priv/trl_worker/uv.lock b/priv/trl_worker/uv.lock index 8241ad1f..a48f4c91 100644 --- a/priv/trl_worker/uv.lock +++ b/priv/trl_worker/uv.lock @@ -171,7 +171,7 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, @@ -196,43 +196,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufft = [ - { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufile = [ - { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] curand = [ - { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] [[package]] @@ -416,7 +416,7 @@ requires-dist = [ { name = "datasets", specifier = "==5.0.1" }, { name = "peft", specifier = "==0.18.1" }, { name = "torch", specifier = "==2.13.0" }, - { name = "transformers", specifier = "==5.5.0" }, + { name = "transformers", specifier = "==5.10.1" }, { name = "trl", specifier = "==1.6.0" }, ] @@ -556,7 +556,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -595,7 +595,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -607,7 +607,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -637,9 +637,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -651,7 +651,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -1033,7 +1033,7 @@ wheels = [ [[package]] name = "transformers" -version = "5.5.0" +version = "5.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -1046,9 +1046,9 @@ dependencies = [ { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/9d/fb46e729b461985f41a5740167688b924a4019141e5c164bea77548d3d9e/transformers-5.5.0.tar.gz", hash = "sha256:c8db656cf51c600cd8c75f06b20ef85c72e8b8ff9abc880c5d3e8bc70e0ddcbd", size = 8237745, upload-time = "2026-04-02T16:13:08.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/d3/5b7c2f1a52ff0e57355efdc21554aab7e4602f6592ab4582a34c988ab956/transformers-5.10.1.tar.gz", hash = "sha256:31112d1dcdfcf9934242acbba891f44e2279ff74b9b8ba4595640e0e04195a3a", size = 8798372, upload-time = "2026-06-03T15:37:03.289Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/28/35f7411ff80a3640c1f4fc907dcbb6a65061ebb82f66950e38bfc9f7f740/transformers-5.5.0-py3-none-any.whl", hash = "sha256:821a9ff0961abbb29eb1eb686d78df1c85929fdf213a3fe49dc6bd94f9efa944", size = 10245591, upload-time = "2026-04-02T16:13:03.462Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8c/3119596c7fcd9b8b8d924d5b5ba1bfdfafd6d8738bdd81ca09c60b4a38b3/transformers-5.10.1-py3-none-any.whl", hash = "sha256:ccb919ea1b77338b44d0d45d23f7472081906b1bb6ed8e5f5cf4d692d1da03d4", size = 11003770, upload-time = "2026-06-03T15:37:00.433Z" }, ] [[package]] diff --git a/test/dependency_advisory_mitigation_test.exs b/test/dependency_advisory_mitigation_test.exs new file mode 100644 index 00000000..c33cab73 --- /dev/null +++ b/test/dependency_advisory_mitigation_test.exs @@ -0,0 +1,70 @@ +defmodule DependencyAdvisoryMitigationTest do + @moduledoc """ + Locks the claims that `.audit_ignore` makes about the three open cowlib + advisories. Each ignore entry there says why the advisory cannot reach imp; + these tests fail if that stops being true, so the reasons cannot rot into + prose. + """ + + use ExUnit.Case, async: true + + @vulnerable_functions [ + # EEF-CVE-2026-43969: unvalidated encoder, still unfixed in cowlib 2.20.0. + {:cow_cookie, :cookie, 1}, + # EEF-CVE-2026-43971: fixed in cowlib 2.20.0, kept here as a floor. + {:cow_link, :link, 1} + ] + + test "no module in the imp application calls a cowlib function under advisory" do + imports = application_imports(:imp) + + for mfa <- @vulnerable_functions do + refute mfa in imports + end + + refute Enum.any?(imports, fn {module, _function, _arity} -> + module == :cow_http_struct_hd + end) + end + + test "cowboy carries the EEF-CVE-2026-43966 response-header mitigation" do + _ = Application.load(:cowboy) + version = :cowboy |> Application.spec(:vsn) |> List.to_string() + + assert Version.compare(version, "2.16.0") in [:eq, :gt], + "cowboy #{version} predates the invalid_response_headers option" + + # Default options: cowboy must refuse a response header carrying CR or LF + # rather than serialising cowlib's unescaped bytes onto the socket. + assert :error_terminate = + :cowboy_http.validate_response_headers( + %{<<"x-test">> => <<"safe\r\nx-injected: true">>}, + %{} + ) + + assert :ok = :cowboy_http.validate_response_headers(%{<<"x-test">> => <<"safe">>}, %{}) + end + + test "Plug refuses the same bytes one layer above cowboy" do + conn = Plug.Test.conn(:get, "/") + + for invalid <- ["safe\r\nx-injected: true", "safe\n", <<"safe", 0>>] do + assert_raise Plug.Conn.InvalidHeaderError, fn -> + Plug.Conn.put_resp_header(conn, "x-test", invalid) + end + end + end + + defp application_imports(application) do + {:ok, modules} = :application.get_key(application, :modules) + + Enum.flat_map(modules, fn module -> + {:module, ^module} = Code.ensure_loaded(module) + + case :beam_lib.chunks(:code.which(module), [:imports]) do + {:ok, {_module, [imports: imports]}} -> imports + _unavailable -> [] + end + end) + end +end diff --git a/test/failure_campaign_test.exs b/test/failure_campaign_test.exs index 67c168ab..7e2c164b 100644 --- a/test/failure_campaign_test.exs +++ b/test/failure_campaign_test.exs @@ -47,6 +47,9 @@ defmodule Imp.FailureCampaignTest do "remaining_live_lanes" => 2 } + assert get_in(artifact, ["configuration", "runtime_warmup", "warmed_lanes"]) == + artifact["scope"] + assert artifact["runtime"]["leak_free"] assert artifact["runtime"]["leaks"] == %{