Skip to content

Secret-leak checks in tests look for a sentinel that a timestamp can't contain - #4860

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/secret-checks-sentinel
Sep 30, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/secret-checks-sentinel

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Why

PgTargetDeadlockDrillDownTests.APlantedCounterDeltaBesideFourCapturedRows_CarriesTheExemplarDrillDown_AndTheRefrozenAdvice failed once on a run where nothing leaked. Its check Assert.DoesNotContain("4721", analysis, ...) is meant to prove that the planted literal held-4721 was normalized away. But analysis is the whole analyze_server JSON, and that JSON also holds wall-clock timestamps. On the failing run the timestamp 2026-09-30T11:20:24.4721267Z has 4721 in its fractional seconds, so the bare substring matched a clock reading, not a secret.

The same can happen to any bare short number checked against text that holds a timestamp, a generated id or a hash.

What changes

Test files only. No product code changes.

The sentinel

Every planted secret literal in Darling/Darling.Tests/PgTargetDeadlockDrillDownTests.cs keeps its digits and gains letters outside a-f. A timestamp, an id or a hash holds only decimal digits or hex digits a-f, so none of them can contain the sentinel:

Was Now Where
'4721' 'qx4721' the unit test's planted statement and graph (line 122, 123)
'9034' 'qx9034' the unit test's planted graph (line 123)
'held-4721' 'held-qx4721' the live test's graph (line 372), the Replace that builds the expected graph (line 422), the raw victim statement (line 461)
"4721", "9034" in the leak loop "qx4721", "qx9034" line 168
DoesNotContain("4721", analysis) DoesNotContain("qx4721", analysis) line 424
DoesNotContain("4721", stored) DoesNotContain("qx4721", stored) line 501

A one-line comment where the pattern is first used (line 121) says why. The 16-digit card numbers and the hashes stay as they were: a timestamp or an id cannot hold them. Every assertion the tests had is kept. All the planted values sit inside string literals, which the normalizer replaces with ? whatever they hold, so the expected normalized text does not change.

The sweep of other bare short-number negative checks

git grep -nE 'DoesNotContain\("[0-9]{3,6}"' -- 'Darling/Darling.Tests/*.cs' 'Lite.Tests/*.cs' found 26 lines besides the two above. For each, the question was whether the searched text can hold a run-varying number. Such numbers are a wall-clock time, a generated id, a hash of run-varying input and a duration measured at run time. None of the 26 can hold one, so none changed. The four loop forms the grep cannot see are classified after the table.

File:line Value What the check reads Verdict
Darling.Tests/ComposeAnnotationServerClockTests.cs:102 240 compiled SQL text deterministic
Darling.Tests/DarlingPgTopQueriesLiveTests.cs:181 42702 the not_collected sentence, built from a constant server name, an engine description and a collector name deterministic
Darling.Tests/DarlingStoreUpgradeTests.cs:1005 50432 pg_upgrade arguments built from fixed paths and two constant ports deterministic
Darling.Tests/McpToolGuideHeads.SqlTail.cs:200 755 a tool guide's served head and tail, fixed text deterministic
Darling.Tests/PgFactCollectorTests.cs:146 3600 SQL text deterministic
Darling.Tests/PgLogEventMetricsTests.cs:274 1007 message, detail, context, fingerprint and relation of events classified from a fixed log text under a fixed hash key deterministic
Darling.Tests/PgLogEventsPipelineTests.cs:1491 6789 messages of events classified from a fixed log text deterministic
Darling.Tests/PgLogEventsPipelineTests.cs:1531 4111 messages of events classified from a fixed log text deterministic
Darling.Tests/PgTargetBlockingTests.cs:560 600000 SQL text deterministic
Darling.Tests/PgTargetBufferDrillDownTests.cs:146 8192 source code with comments and strings stripped deterministic
Darling.Tests/PgTargetFactCollectorTests.cs:299 3600 SQL text deterministic
Darling.Tests/PgTargetSampledWaitTests.cs:59 30000 SQL text deterministic
Darling.Tests/PgTargetSessionsTests.cs:558 60000 SQL text deterministic
Darling.Tests/PgTargetSessionsTests.cs:1350 900000 advice headline and investigation composed from fixed inputs deterministic
Darling.Tests/PgTargetWaitTests.cs:298 30000 SQL text deterministic
Darling.Tests/PgTargetWaitTests.cs:303 30000 source code with comments and strings stripped deterministic
Darling.Tests/PostgresFaultOutcomeTests.cs:1006 57014 a timeout explanation built from fixed arguments deterministic
Darling.Tests/StoreLogClassifierTests.cs:541 250000 message text and sample line of groups classified from fixed log lines deterministic
Darling.Tests/StoreLogClassifierTests.cs:756 4111 message text and sample line of groups classified from fixed log lines deterministic
Darling.Tests/ViewerPostgresTabsTests.cs:464 1024 PgDisplay.Bytes of three fixed byte counts deterministic
Darling.Tests/WebExceptionTextCensusTests.cs:539 57014 the error field, which the line above already asserts equals the fixed timeout message deterministic
Darling.Tests/WebExceptionTextCensusTests.cs:1006 57014 a response body that holds only the fixed timeout message deterministic
Lite.Tests/McpToolGuideHeads.SqlTail.cs:118 755 a tool guide's served head and tail, fixed text deterministic
Lite.Tests/PgDeadlockLogParserTests.cs:410 4111 graph text of a report parsed from a fixed log text deterministic
Lite.Tests/PgDeadlockLogParserTests.cs:411 9034 graph text of a report parsed from a fixed log text deterministic
Lite.Tests/PgPlanCaptureCollectorDefinitionTests.cs:122 100 the redacted filter of a fixed JSON plan deterministic

The loop forms:

File:line Values What the loop feeds Verdict
Lite.Tests/PgDeadlockLogParserTests.cs:357 4721, 9034 (and a name and two card numbers) graph text and victim statement parsed from a fixed log text deterministic
Darling.Tests/AlertReadFailureSurfaceTests.cs:86 101, 202 server keys fed to equality assertions on a counter, no text search not a negative check
Darling.Tests/AlertReadFailureSurfaceTests.cs:174 101 server keys fed to equality assertions on a counter, no text search not a negative check
Darling.Tests/StartupFailureTriageTests.cs:158 57014, 57P04 SQLSTATE codes fed to Assert.False(IsRetryable(...)), a classification and not a text search not a negative check

Test plan

  • Darling.Tests and Lite.Tests build with 0 Warning(s).

  • PgTargetDeadlockDrillDownTests: Total: 9, Errors: 0, Failed: 0, Skipped: 1, Not Run: 0 (the skipped one is the live test).

  • TsqlConventionGuardTests: Total: 14, Errors: 0, Failed: 0. DocCommentHygieneTests: Total: 77, Errors: 0, Failed: 0.

  • RED 1, the exemplar path keeps the raw literal. NormalizeStoredDeadlockExemplars was changed to leave the victim statement and the graph text as stored, then restored. The unit test fails at its exact-value assertion, which comes before the sentinel loop is reached:

    Darling.Tests.PgTargetDeadlockDrillDownTests.AStoredFindingsExemplars_ReadBackNormalized_AndNameTheirReportByTimeAndPid [FAIL]
      Assert.Equal() Failure: Strings differ
      Expected: ···"DATE accounts SET pin = '?' WHERE card = ?"
      Actual:   ···"DATE accounts SET pin = 'qx4721' WHERE card = 4111"···
        PgTargetDeadlockDrillDownTests.cs(158,0)
    Total: 9, Errors: 0, Failed: 1, Skipped: 1, Not Run: 0
    
  • RED 2, a leak only the sentinel loop can see. NormalizeStoredDeadlockExemplars was changed to leave the prose note as stored (every exact-value assertion still holds), then restored. The changed leak loop catches it:

    Darling.Tests.PgTargetDeadlockDrillDownTests.AStoredFindingsExemplars_ReadBackNormalized_AndNameTheirReportByTimeAndPid [FAIL]
      Assert.DoesNotContain() Failure: Sub-string found
      String: ···"accounts SET pin = \\u0027qx4721\\u0027 WHERE card ="···
      Found:  "qx4721"
        PgTargetDeadlockDrillDownTests.cs(169,0)
    Total: 9, Errors: 0, Failed: 1, Skipped: 1, Not Run: 0
    
  • Both plants were restored: git diff shows the one test file.

  • Full Darling.Tests suite, once, with no live database: Darling.Tests Total: 18891, Errors: 0, Failed: 0, Skipped: 1190, Not Run: 1, Time: 94.116s. The skips are live tests that need PostgreSQL, and the one not run is an explicit test.

  • The two live checks (lines 424 and 501) run only in CI against PostgreSQL. They passed in CI at 5b6da3a against PostgreSQL. They were not run here, so no RED was shown for them.

  • Lite.Tests suite: no Lite test changed, so only its build was run here. CI ran the Lite shards green.

CHANGELOG

SECTION: None

…an't contain

The deadlock drill-down tests planted 4721 and 9034 and then searched whole JSON responses for those digits. A wall-clock timestamp such as 11:20:24.4721267 holds them in its fractional seconds, so one run failed with nothing leaking. The planted literals are now qx4721 and qx9034 (letters outside a-f), which no timestamp, decimal id or hex hash can contain. Every assertion is kept.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 30, 2026 12:19
@erikdarlingdata
erikdarlingdata merged commit 107d0f2 into dev Sep 30, 2026
17 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/secret-checks-sentinel branch September 30, 2026 12:20
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