Skip to content

fix: cap AsYouTypeFormatter growth and guard PhoneNumberUtil against null - #402

Merged
twcclegg merged 2 commits into
mainfrom
fix/asyoutypeformatter-dos-and-nre-guards
Aug 25, 2026
Merged

fix: cap AsYouTypeFormatter growth and guard PhoneNumberUtil against null#402
twcclegg merged 2 commits into
mainfrom
fix/asyoutypeformatter-dos-and-nre-guards

Conversation

@twcclegg

Copy link
Copy Markdown
Owner

Summary

  • AsYouTypeFormatter.InputDigit re-derives its output from the full accrued buffers on every call (including a regex match against the whole accumulated nationalNumber, plus a ToString() of the ever-growing accruedInput on most bail-out paths). With no cap on input length, per-call cost grows worse than quadratically with digits typed — measured 500k digits taking 30s+, extrapolating past two minutes. Real national significant numbers plus a parsed extension never approach 50 characters, so past that the formatter now freezes its output and every further call is O(1).
  • Upstream Java has the identical shape (full matches() against the whole accumulated nationalNumber in attemptToFormatAccruedDigits, no length cap), so this isn't a porting bug — it's a defensive cap added here, in the same spirit as the existing extension-digit length limits, that changes nothing for any real phone number.
  • Several PhoneNumberUtil entry points dereferenced a null PhoneNumber argument several frames deep instead of failing fast at the boundary, surfacing a bare NullReferenceException instead of a typed one. IsViablePhoneNumber, Format, IsValidNumber, IsPossibleNumber, GetRegionCodeForNumber (which GetNumberType calls first), and IsNumberMatch now throw ArgumentNullException.

Test plan

  • dotnet build csharp — clean, 0 warnings/errors, all TFMs (netstandard2.0/net8.0/net10.0)
  • dotnet test csharp/PhoneNumbers.slnx — full matrix, 432/432 passing (net8.0 + net10.0)
  • Empirically verified the formatter fix: 2,000,000 digits fed one at a time now takes ~1ms (was hanging past 15s, extrapolated ~2 minutes) via an ad hoc stress harness against the built library
  • Verified normal formatting is unaffected ((415) 666-7777, +44 117 496 0123, etc.)
  • Verified all previously-crashing null-arg call sites now throw ArgumentNullException with the correct ParamName

…null

AsYouTypeFormatter.InputDigit re-derives its return value from the full
accrued buffers on every call (including a regex match against the whole
accumulated nationalNumber, and a ToString() of the ever-growing
accruedInput on most bail-out paths), so feeding it digits with no upper
bound makes per-call cost grow worse than quadratically with the number of
digits typed - confirmed empirically: 500k digits took 30s+, extrapolating
past two minutes. Real national significant numbers plus a parsed
extension never approach 50 characters, so past that we freeze the output
and every further call is O(1).

Upstream Java has the identical shape (full regex match against the whole
accumulated nationalNumber in attemptToFormatAccruedDigits, no length
cap), so this isn't a porting bug - it's a defensive cap added here that
changes nothing for any real phone number, in the same spirit as the
existing extension-digit length limits.

Separately, several PhoneNumberUtil entry points dereferenced a null
PhoneNumber argument several frames deep instead of failing fast at the
boundary, surfacing as a bare NullReferenceException instead of a typed
one: IsViablePhoneNumber, Format, IsValidNumber, IsPossibleNumber,
GetRegionCodeForNumber (which GetNumberType calls first), and
IsNumberMatch now throw ArgumentNullException instead.
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 30.00000% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.05%. Comparing base (a40ff86) to head (0834189).

Files with missing lines Patch % Lines
csharp/PhoneNumbers/PhoneNumberUtil.cs 0.00% 6 Missing and 6 partials ⚠️
csharp/PhoneNumbers/PhoneNumberUtil.net.cs 0.00% 1 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (30.00%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #402      +/-   ##
==========================================
- Coverage   77.26%   77.05%   -0.21%     
==========================================
  Files          39       39              
  Lines        4548     4568      +20     
  Branches     1129     1138       +9     
==========================================
+ Hits         3514     3520       +6     
- Misses        783      790       +7     
- Partials      251      258       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

📊 Benchmark Results

Commit: 0834189 · Full run · Linux ubuntu-24.04-arm

Both sides were measured on the same runner in the same job, so the numbers are
comparable. Treat sub-percent differences as noise.

PR branch

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
InputDigitPerKeystroke 1000 4.388 ms 0.0098 ms 0.0091 ms 54.6875 3.88 MB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  Job-AMQORM : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Runtime=.NET 10.0  InvocationCount=1  IterationCount=20  
LaunchCount=1  RunStrategy=ColdStart  UnrollFactor=1  
WarmupCount=1  

Method Mean Error StdDev Allocated
CreateInstance 389.6 μs 105.0 μs 120.9 μs 119.48 KB
CreateInstanceAndLoadAllRegions 7,017.4 μs 354.6 μs 408.4 μs 1620.34 KB
FirstRegionLookup 435.0 μs 127.6 μs 147.0 μs 124.54 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
ExtractPossibleNumber_CleanInput 1000 20.95 μs 0.018 μs 0.016 μs - -
ExtractPossibleNumber_WithLeadingJunk 1000 38.01 μs 0.058 μs 0.055 μs 0.6714 48360 B

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
FindNumbers_Valid 100 138.3 μs 0.29 μs 0.27 μs 0.9766 70.71 KB
FindNumbers_StrictGrouping 100 309.3 μs 0.40 μs 0.38 μs 1.4648 123.99 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
GetDescriptionForNumber 1000 1,429.22 μs 1.480 μs 1.312 μs 1.9531 196.59 KB
GetDisplayCountry 1000 18.52 μs 0.040 μs 0.035 μs 0.0916 7.56 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
ParseValidateAndFormatPhoneNumbers 1000 2,510.3 μs 2.25 μs 1.88 μs 7.8125 577.71 KB
ParseOnly 1000 432.4 μs 0.55 μs 0.48 μs 4.8828 348.13 KB
ParseNationalFormat 1000 767.2 μs 1.26 μs 1.18 μs 5.8594 431.36 KB
ParseWithExtension 1000 1,108.7 μs 5.12 μs 4.54 μs 13.6719 957.38 KB
ValidateOnly 1000 732.8 μs 0.54 μs 0.48 μs - 41.11 KB
FormatOnly 1000 1,062.2 μs 5.22 μs 4.62 μs 1.9531 187.92 KB
PR base

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
InputDigitPerKeystroke 1000 4.341 ms 0.0119 ms 0.0112 ms 54.6875 3.87 MB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  Job-AMQORM : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Runtime=.NET 10.0  InvocationCount=1  IterationCount=20  
LaunchCount=1  RunStrategy=ColdStart  UnrollFactor=1  
WarmupCount=1  

Method Mean Error StdDev Allocated
CreateInstance 385.9 μs 112.0 μs 128.9 μs 119.48 KB
CreateInstanceAndLoadAllRegions 7,053.1 μs 360.7 μs 415.3 μs 1620.34 KB
FirstRegionLookup 451.8 μs 133.9 μs 154.2 μs 124.54 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
ExtractPossibleNumber_CleanInput 1000 21.24 μs 0.013 μs 0.012 μs - -
ExtractPossibleNumber_WithLeadingJunk 1000 37.62 μs 0.093 μs 0.082 μs 0.6714 48360 B

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
FindNumbers_Valid 100 142.0 μs 0.26 μs 0.24 μs 0.9766 70.71 KB
FindNumbers_StrictGrouping 100 311.9 μs 0.71 μs 0.67 μs 1.4648 123.99 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
GetDescriptionForNumber 1000 1,406.85 μs 1.625 μs 1.269 μs 1.9531 196.59 KB
GetDisplayCountry 1000 18.55 μs 0.023 μs 0.021 μs 0.0916 7.56 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
ParseValidateAndFormatPhoneNumbers 1000 2,465.4 μs 8.42 μs 7.46 μs 7.8125 577.71 KB
ParseOnly 1000 438.1 μs 0.93 μs 0.87 μs 4.8828 348.13 KB
ParseNationalFormat 1000 760.6 μs 1.54 μs 1.44 μs 5.8594 431.36 KB
ParseWithExtension 1000 1,105.3 μs 4.75 μs 4.44 μs 13.6719 957.38 KB
ValidateOnly 1000 744.6 μs 4.50 μs 3.99 μs - 41.11 KB
FormatOnly 1000 1,054.2 μs 1.81 μs 1.60 μs 1.9531 187.92 KB

- Raise the cap from an arbitrary 50 characters to
  PhoneNumberUtil.MAX_INPUT_STRING_LENGTH (250, made internal to
  share it) - the same bound Parse() itself uses to decide something
  isn't plausibly a phone number. 50 was uncomfortably close to
  realistic input (a national number plus a verbose extension and a
  few pause characters can already run 40+ characters), so legitimate
  as-you-type entries could silently freeze mid-input. 250 leaves
  generous headroom for any realistic entry while still bounding cost
  for genuinely pathological input.
- cappedOutput is now properly `string?` (scoped #nullable
  enable/restore, since this file is nullable-oblivious on
  netstandard2.0) instead of a non-nullable field hidden behind
  null!, matching this repo's nullable-annotation convention.

Added regression tests: one confirms output keeps updating (not
frozen) well past the old 50-character threshold, the other confirms
truly pathological input (300 characters) still gets capped at 250.
Comment thread csharp/PhoneNumbers.Test/TestAsYouTypeFormatter.cs Dismissed
@twcclegg

Copy link
Copy Markdown
Owner Author

Fixed both review findings: raised the cap from an arbitrary 50 chars to PhoneNumberUtil.MAX_INPUT_STRING_LENGTH (250, same bound Parse() itself uses) so realistic long entries no longer silently freeze mid-input, and cappedOutput is now properly string? instead of hidden behind null!. Added regression tests for both the not-frozen and still-capped-eventually behavior. 868 tests pass.

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.

2 participants