Skip to content

Refuse a non-finite min_duration_ms (#3287) - #3369

Merged
erikdarlingdata merged 2 commits into
devfrom
fix/3287-nonfinite-floor
Sep 12, 2026
Merged

Refuse a non-finite min_duration_ms (#3287)#3369
erikdarlingdata merged 2 commits into
devfrom
fix/3287-nonfinite-floor

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 12, 2026

Copy link
Copy Markdown
Owner

ValidateMinMs tested only minMs is < 0. NaN fails that like it fails every comparison, +Infinity is not negative, and an oversized literal such as 1e400 parses, to +Infinity, rather than being rejected as too large — so all four spellings arrived as accepted floors. None can ever match a run, so the read then returned the filtered-no-matches status, which names the floor it applied and reads as a true negative about the window. A refusal rendered as an answer, in the one validator whose whole job is refusing values that cannot mean what they say.

All of them are reachable from the web surface: double.TryParse under NumberStyles.Float accepts the named literals regardless of the style flags, and the overflow case parses rather than failing. Measured, all four.

!double.IsFinite is tested ahead of the sign so -Infinity is refused for what it is rather than for its sign. 0 remains a real value — it admits every row and ranks the page by duration — and the negative message keeps its wording, which pins on both SKUs assert on.

Two lanes reached this independently, and this carries the better-instrumented half of each

The #3287 lane found it from the same review comment and had already written a fix on its own branch. Its version is better in two ways and both are taken here rather than defended against:

  • A positive control on both SKUs — a finite floor on the same read still returns a page, so the refusal assertions are not passing against a read that refuses everything.
  • The mechanism documented in the <summary>, where a caller reading the API sees it, rather than in a comment inside the method body.

What is kept from the other version is one extra assertion: the refusal must not contain the no-matches message either, asserted beside the status-token check. The status is the branch taken and the sentence is what the caller actually reads, and the two can drift.

Pinned end-to-end on both SKUs over NaN, +Infinity and -Infinity, with the finite positive control. The assertions that discriminate are the two absences: before this, the value passed validation and fell into the no-matches branch, so asserting only the lack of a payload would have been green on the defect.

Found by the review bot on #3363 and merged past — this is that finding, repaired.

NaN fails every comparison including the sign test, +Infinity is not
negative, and an overflowing literal parses to +Infinity rather than
failing to bind. All three were accepted as floors, matched no row, and
returned the filtered-no-matches status naming a floor that was never a
number - a refusal rendered as an answer.
@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown

Reviewed. The fix is correct and matches the PR description precisely:

  • ValidateMinMs now tests !double.IsFinite(floor) before the sign check, which correctly catches NaN (fails every comparison, including < 0), +Infinity (not negative), and -Infinity (previously caught by the sign test but now given the accurate "not a real number" message instead).
  • The validator lives in PerformanceMonitor.Common, and both Darling/.../DarlingMcpDataTools.cs and Lite/Mcp/McpHealthTools.cs call the same shared helper — so Lite/Darling parity is automatic here, no drift risk.
  • The new tests in both Darling.Tests/DarlingCollectionLogReadTests.cs and Lite.Tests/CollectionLogToolTests.cs are parallel in structure and assertions (same message substring, same DoesNotContain("matched min_duration_ms", ...) discriminator), which is the right way to keep the two SKUs honest.
  • Ms(double?) renders NaN/Infinity/-Infinity via ToString(CultureInfo.InvariantCulture) without throwing, so the error message itself doesn't blow up on these inputs.

One gap worth considering before/after merge: the PR body specifically identifies the web endpoint's ?min_duration_ms= query string as the reachable path for these values (double.TryParse(..., NumberStyles.Float, ...) accepts the NaN/Infinity literals and silently overflows 1e400 to +Infinity). But the new tests only call GetCollectionLog(..., min_duration_ms: notANumber) with a C# double literal directly — they don't exercise DarlingWebEndpoints.TryParseOptionalDouble, which is the actual string-parsing boundary where the bug was reachable from an HTTP caller. The existing theory at Darling/Darling.Tests/DarlingWebEndpointsTests.cs:188-203 (TryParseOptionalDouble_RefusesGarbageRatherThanDroppingTheFilter) would be a natural place to add InlineData("NaN", true, double.NaN), InlineData("Infinity", true, double.PositiveInfinity), InlineData("-Infinity", true, double.NegativeInfinity), and InlineData("1e400", true, double.PositiveInfinity) cases, so there's an end-to-end pin on the actual attacker-reachable surface rather than only on the tool-layer validator. (Lite has no web endpoint, so this is Darling-only.)

Not a blocker — the validator fix closes the hole for every caller regardless — but it's the one place the test suite doesn't yet match the PR's own narrative about where this was reachable from.

The sibling lane reached this defect independently and its version carries
a positive control mine did not: a finite floor on the same read still
returns a page, so the three refusal assertions are not passing against a
read that refuses everything. It also documents the mechanism, including
the 1e400 overflow, in the summary a caller reads rather than in a comment
inside the method.

Keeps the no-matches MESSAGE assertion beside its status-token one: the
status is the branch taken, the sentence is what the caller reads, and the
two can drift.
@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown

Re-reviewed at the current head (after the "better-instrumented form of the same fix" commit) — confirms the analysis in the earlier review comment still holds:

  • ValidateMinMs checks !double.IsFinite(value) before the sign check, so NaN, +Infinity, and -Infinity all get the new "must be a finite number" message rather than falling through the old < 0 branch (which NaN/+Infinity would have passed silently, and -Infinity would have hit with the wrong wording).
  • The validator lives in PerformanceMonitor.Common and both DarlingMcpDataTools.GetCollectionLog and McpHealthTools.GetCollectionLog call the same shared helper — Lite/Darling parity is structural here, not something a reviewer has to cross-check by hand. Grepped for other caller-supplied double? MCP parameters; min_duration_ms is the only one, so the fix's scope is complete.
  • The two test files add parallel coverage (same three non-finite values, same message/status/empty/"runs" assertions, same positive control), which is the right way to keep the SKUs honest.

One gap from the earlier review is still unaddressed as of the latest commit: Darling/Darling.Tests/DarlingWebEndpointsTests.cs:188-196 (TryParseOptionalDouble_RefusesGarbageRatherThanDroppingTheFilter) is the actual string-parsing boundary the PR's own reasoning points to (double.TryParse under NumberStyles.Float accepting "NaN"/"Infinity"/"-Infinity" literals and "1e400" overflowing to +Infinity), but its [InlineData] set still only covers "abc" as the unreadable case. Adding InlineData("NaN", true, double.NaN), InlineData("Infinity", true, double.PositiveInfinity), InlineData("-Infinity", true, double.NegativeInfinity), and InlineData("1e400", true, double.PositiveInfinity) would pin the actual HTTP-reachable surface end-to-end instead of only the tool-layer validator. Not a blocker — ValidateMinMs closes the hole regardless of how the double got there — just the one place the test suite doesn't yet match the PR's own narrative about where this was reachable from.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant