Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .audit_ignore
Original file line number Diff line number Diff line change
@@ -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/<id>), the GitHub Advisory Database
# (`gh api /advisories?cve_id=<cve>`), 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(<<C,R/bits>>, Acc) -> escape_string(R,
# <<Acc/binary,C>>)`), 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
4 changes: 2 additions & 2 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}},
Expand Down Expand Up @@ -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}}
]
59 changes: 36 additions & 23 deletions bench/imp/benchmark_truth/failure_campaign.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/requirements-ifbench-parity.txt
Original file line number Diff line number Diff line change
@@ -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
32 changes: 31 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"
]
]
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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"},
}
2 changes: 1 addition & 1 deletion priv/trl_worker/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down
Loading
Loading