From f67df05b6a100e275288390cb79b1ef71e631ca1 Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Sat, 26 Sep 2026 07:15:57 -0400
Subject: [PATCH 1/3] Warm the settings redactor's patterns before their first
timed match (#4348)
---
.../Darling.Tests/PgSettingRedactorTests.cs | 18 +++++++
.../PgSettingRedactor.cs | 49 +++++++++++++++++++
2 files changed, 67 insertions(+)
diff --git a/Darling/Darling.Tests/PgSettingRedactorTests.cs b/Darling/Darling.Tests/PgSettingRedactorTests.cs
index 6a9849cdb3..91a4b47840 100644
--- a/Darling/Darling.Tests/PgSettingRedactorTests.cs
+++ b/Darling/Darling.Tests/PgSettingRedactorTests.cs
@@ -518,4 +518,22 @@ public void ForcedTimeout_ThrowingCallback_StillReturnsMaskAndDoesNotThrow()
PgSettingRedactor.MatchTimeoutForTest = null;
}
}
+
+ ///
+ /// #4348: every Compiled pattern is warmed at type initialisation, before any real
+ /// call can return, so the first real call never pays a
+ /// first-use JIT cost against the 100ms match timeout. This pins that the warmup ran (rather than
+ /// re-deriving that fact from timing, which would be flaky by construction on a busy runner) and that a
+ /// short, well-formed value is masked in PART, not masked whole, the way a spurious timeout would
+ /// produce.
+ ///
+ [Fact]
+ public void Warmup_RanBeforeFirstRealCall_AndShortValueIsNotMaskedWhole()
+ {
+ Assert.True(PgSettingRedactor.WarmedUp);
+
+ var result = PgSettingRedactor.Redact("archive_command", "PGPASSWORD=hunter2 psql -c 'select 1'");
+
+ Assert.Equal("PGPASSWORD=******** psql -c 'select 1'", result);
+ }
}
diff --git a/PerformanceMonitor.Collectors/PgSettingRedactor.cs b/PerformanceMonitor.Collectors/PgSettingRedactor.cs
index bfcabc086c..f0d8df3bd5 100644
--- a/PerformanceMonitor.Collectors/PgSettingRedactor.cs
+++ b/PerformanceMonitor.Collectors/PgSettingRedactor.cs
@@ -114,6 +114,25 @@ public string Replace(string input, MatchEvaluator evaluator) =>
(MatchTimeoutForTest is TimeSpan overrideTimeout
? new Regex(_pattern, _options, overrideTimeout)
: _default).Replace(input, evaluator);
+
+ /// Runs the DEFAULT (production-timeout) instance once on a short, benign string, outside
+ /// any timed accounting the caller does (#4348): a pattern's IL
+ /// is JITted on its first invocation, and on a busy CI runner that JIT cost alone can exceed the
+ /// 100 ms , turning the first real call on a short, well-formed value
+ /// into a spurious whole-value mask. Warming here, at type initialisation, pays that cost once,
+ /// before the type is usable at all, so the first real call never has to. A
+ /// timeout during warmup (the JIT itself, on an especially slow runner, taking longer than the
+ /// timeout) is swallowed — warmup exists to pre-pay JIT cost, not to prove the pattern is fast.
+ public void Warmup()
+ {
+ try
+ {
+ _default.IsMatch("x");
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ }
+ }
}
/// Names whose value is masked in full when the setting is extension-scoped (#4348), or when a
@@ -277,6 +296,36 @@ public string Replace(string input, MatchEvaluator evaluator) =>
"pwd",
};
+ /// Set once, before any real call can return (#4348): the static
+ /// constructor below warms every pattern's first-use JIT cost before it counts
+ /// against anyone's . Exposed only so a test can pin that warmup ran ahead of
+ /// the first real call, without the test having to spin up a fresh AssemblyLoadContext just to
+ /// observe type-initialisation order.
+ internal static readonly bool WarmedUp;
+
+ static PgSettingRedactor()
+ {
+ LibpqPasswordKeyword.Warmup();
+ UriQueryPassword.Warmup();
+ AssignmentSecretName.Warmup();
+ OptionSecretSpaced.Warmup();
+ UriQueryKeyAnyEncoding.Warmup();
+ UriQuerySignature.Warmup();
+ QuotedSpacedAssignment.Warmup();
+ CurlUserColon.Warmup();
+ SshpassOption.Warmup();
+
+ try
+ {
+ UriUserInfoPassword.IsMatch("x");
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ }
+
+ WarmedUp = true;
+ }
+
private static bool IsQueryKeySecret(string rawKey)
{
string decoded;
From 352f2171e4f667cca3c2aad8eccdc84e54cf61d5 Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Sat, 26 Sep 2026 07:38:37 -0400
Subject: [PATCH 2/3] Warm each redactor pattern through its match step (#4348)
---
.../Darling.Tests/PgSettingRedactorTests.cs | 17 +++++++
.../PgSettingRedactor.cs | 49 ++++++++++++++-----
2 files changed, 55 insertions(+), 11 deletions(-)
diff --git a/Darling/Darling.Tests/PgSettingRedactorTests.cs b/Darling/Darling.Tests/PgSettingRedactorTests.cs
index 91a4b47840..71356034cd 100644
--- a/Darling/Darling.Tests/PgSettingRedactorTests.cs
+++ b/Darling/Darling.Tests/PgSettingRedactorTests.cs
@@ -536,4 +536,21 @@ public void Warmup_RanBeforeFirstRealCall_AndShortValueIsNotMaskedWhole()
Assert.Equal("PGPASSWORD=******** psql -c 'select 1'", result);
}
+
+ ///
+ /// #4348: warmup must reach each pattern's MATCH step, not only its scan (TryFindNextPossibleStartingPosition
+ /// finding a candidate but TryMatchAtCurrentPosition never running). This pins that the warmup
+ /// sample every is warmed with actually produces at
+ /// least one match for EVERY pattern in the list, so the match step is exercised, not skipped.
+ ///
+ [Fact]
+ public void WarmupSample_MatchesEveryWarmedPattern()
+ {
+ Assert.NotEmpty(PgSettingRedactor.WarmedPatterns);
+
+ foreach (var pattern in PgSettingRedactor.WarmedPatterns)
+ {
+ Assert.True(pattern.WarmupSampleMatches());
+ }
+ }
}
diff --git a/PerformanceMonitor.Collectors/PgSettingRedactor.cs b/PerformanceMonitor.Collectors/PgSettingRedactor.cs
index f0d8df3bd5..b11edecd5b 100644
--- a/PerformanceMonitor.Collectors/PgSettingRedactor.cs
+++ b/PerformanceMonitor.Collectors/PgSettingRedactor.cs
@@ -66,6 +66,11 @@ public static class PgSettingRedactor
private const string Mask = "********";
+ /// A sample that reaches every pattern's match step (#4348), used only to warm each compiled
+ /// pattern's JIT ahead of the first real call. Holds placeholder values only.
+ private const string WarmupSample =
+ "password=x sslpassword=y postgresql://u:p@h/?password=z&sig=s&X-Amz-Signature=t \"password\" = \"q\" --password w sshpass -p v curl -u a:b";
+
///
/// A per-regex match-time bound (#4348): a pathological value (a long run with no separator, feeding one
/// of the lookaround-heavy patterns below) could otherwise pin the engine backtracking well past the
@@ -97,7 +102,7 @@ internal static TimeSpan? MatchTimeoutForTest
/// Wraps one lookaround-bearing pattern so it always runs under a match timeout, while still
/// letting a test force a much shorter one via without rebuilding
/// every call on the production path.
- private sealed class TimeBoundPattern
+ internal sealed class TimeBoundPattern
{
private readonly string _pattern;
private readonly RegexOptions _options;
@@ -115,24 +120,30 @@ public string Replace(string input, MatchEvaluator evaluator) =>
? new Regex(_pattern, _options, overrideTimeout)
: _default).Replace(input, evaluator);
- /// Runs the DEFAULT (production-timeout) instance once on a short, benign string, outside
- /// any timed accounting the caller does (#4348): a pattern's IL
- /// is JITted on its first invocation, and on a busy CI runner that JIT cost alone can exceed the
- /// 100 ms , turning the first real call on a short, well-formed value
- /// into a spurious whole-value mask. Warming here, at type initialisation, pays that cost once,
- /// before the type is usable at all, so the first real call never has to. A
- /// timeout during warmup (the JIT itself, on an especially slow runner, taking longer than the
- /// timeout) is swallowed — warmup exists to pre-pay JIT cost, not to prove the pattern is fast.
+ /// Runs the DEFAULT (production-timeout) instance once on a sample that reaches every
+ /// pattern's match step, outside any timed accounting the caller does (#4348): a
+ /// pattern's IL is JITted on its first invocation, and on a busy
+ /// CI runner that JIT cost alone can exceed the 100 ms , turning the
+ /// first real call on a short, well-formed value into a spurious whole-value mask. Warming here, at
+ /// type initialisation, pays that cost once, before the type is usable at all, so the first real
+ /// call never has to. A timeout during warmup (the JIT itself, on an especially
+ /// slow runner, taking longer than the timeout) is swallowed — warmup exists to pre-pay JIT cost,
+ /// not to prove the pattern is fast.
public void Warmup()
{
try
{
- _default.IsMatch("x");
+ _default.IsMatch(WarmupSample);
}
catch (RegexMatchTimeoutException)
{
}
}
+
+ /// Runs 's sample through this pattern and reports whether it produced
+ /// at least one match (#4348) — used only by the test that pins that warmup reaches every pattern's
+ /// match step, not only its scan.
+ internal bool WarmupSampleMatches() => _default.IsMatch(WarmupSample);
}
/// Names whose value is masked in full when the setting is extension-scoped (#4348), or when a
@@ -317,7 +328,7 @@ static PgSettingRedactor()
try
{
- UriUserInfoPassword.IsMatch("x");
+ UriUserInfoPassword.IsMatch(WarmupSample);
}
catch (RegexMatchTimeoutException)
{
@@ -326,6 +337,22 @@ static PgSettingRedactor()
WarmedUp = true;
}
+ /// Every this type warms at type initialisation (#4348) —
+ /// exposed only so a test can pin that the warmup sample actually reaches each one's match step, not
+ /// only its scan.
+ internal static readonly TimeBoundPattern[] WarmedPatterns =
+ {
+ LibpqPasswordKeyword,
+ UriQueryPassword,
+ AssignmentSecretName,
+ OptionSecretSpaced,
+ UriQueryKeyAnyEncoding,
+ UriQuerySignature,
+ QuotedSpacedAssignment,
+ CurlUserColon,
+ SshpassOption,
+ };
+
private static bool IsQueryKeySecret(string rawKey)
{
string decoded;
From 678bb17e0922cf485257516e49d94a7da18af7dc Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Sat, 26 Sep 2026 07:47:05 -0400
Subject: [PATCH 3/3] Warm the redactor through a full redaction, and never
fail type initialisation (#4348)
---
.../PgSettingRedactor.cs | 68 ++++++++++++-------
1 file changed, 43 insertions(+), 25 deletions(-)
diff --git a/PerformanceMonitor.Collectors/PgSettingRedactor.cs b/PerformanceMonitor.Collectors/PgSettingRedactor.cs
index b11edecd5b..ea58d7551d 100644
--- a/PerformanceMonitor.Collectors/PgSettingRedactor.cs
+++ b/PerformanceMonitor.Collectors/PgSettingRedactor.cs
@@ -131,11 +131,15 @@ public string Replace(string input, MatchEvaluator evaluator) =>
/// not to prove the pattern is fast.
public void Warmup()
{
+ // Catch-all, not only the timeout (#4348): ANY exception here, uncaught, would fail the static
+ // constructor and leave the type permanently unusable (every later call throws
+ // TypeInitializationException) — warmup exists to pre-pay a cost, never to gate whether the type
+ // works at all.
try
{
_default.IsMatch(WarmupSample);
}
- catch (RegexMatchTimeoutException)
+ catch
{
}
}
@@ -314,32 +318,11 @@ public void Warmup()
/// observe type-initialisation order.
internal static readonly bool WarmedUp;
- static PgSettingRedactor()
- {
- LibpqPasswordKeyword.Warmup();
- UriQueryPassword.Warmup();
- AssignmentSecretName.Warmup();
- OptionSecretSpaced.Warmup();
- UriQueryKeyAnyEncoding.Warmup();
- UriQuerySignature.Warmup();
- QuotedSpacedAssignment.Warmup();
- CurlUserColon.Warmup();
- SshpassOption.Warmup();
-
- try
- {
- UriUserInfoPassword.IsMatch(WarmupSample);
- }
- catch (RegexMatchTimeoutException)
- {
- }
-
- WarmedUp = true;
- }
-
/// Every this type warms at type initialisation (#4348) —
/// exposed only so a test can pin that the warmup sample actually reaches each one's match step, not
- /// only its scan.
+ /// only its scan. The static constructor below drives its warm-up from THIS list, plus
+ /// if it is not already in it, so a pattern added here can't be
+ /// missed from one of the two.
internal static readonly TimeBoundPattern[] WarmedPatterns =
{
LibpqPasswordKeyword,
@@ -353,6 +336,41 @@ static PgSettingRedactor()
SshpassOption,
};
+ static PgSettingRedactor()
+ {
+ // Catch-all around every warm-up step, not only the timeout (#4348): ANY exception escaping the
+ // static constructor fails type initialisation, and every later Redact call would then throw
+ // TypeInitializationException forever — warmup exists to pre-pay JIT cost, never to gate whether the
+ // type works at all.
+ foreach (var pattern in WarmedPatterns)
+ {
+ pattern.Warmup();
+ }
+
+ try
+ {
+ UriUserInfoPassword.IsMatch(WarmupSample);
+ }
+ catch
+ {
+ }
+
+ // Warms the evaluator lambdas too, not only the patterns' match step (#4348): the match-timeout
+ // clock spans a whole Regex.Replace call, including its MatchEvaluator, so a full Redact call here
+ // JITs those delegates before any real call has to. Safe to call from the static constructor because
+ // every field Redact reads (the patterns above, Mask, QueryKeySecretMarkers) is field-initialised,
+ // and field initialisers run before this constructor body.
+ try
+ {
+ Redact(null, WarmupSample);
+ }
+ catch
+ {
+ }
+
+ WarmedUp = true;
+ }
+
private static bool IsQueryKeySecret(string rawKey)
{
string decoded;