Skip to content

Fixing datetime arithmetic overflow/underflow. - #5781

Open
v-isyamauchi-gh wants to merge 9 commits into
mainfrom
personal/v-isyamauchi/181592
Open

Fixing datetime arithmetic overflow/underflow.#5781
v-isyamauchi-gh wants to merge 9 commits into
mainfrom
personal/v-isyamauchi/181592

Conversation

@v-isyamauchi-gh

Copy link
Copy Markdown
Contributor

Description

Fix: Prevent DateTime overflow in search expression rewriters

FHIR search queries with extreme dates (e.g., _lastUpdated=gt9999-12-31, birthdate=ap9500-01-01) caused OverflowException in the SQL rewriting pipeline → 500 response. The overflow came from internal date arithmetic (AddTicks/AddDays) for SQL optimization, not invalid user input.

Changes:

  • Added DateTimeSafeExtensions with SafeAddTicks/SafeAddDays that clamp to DateTime.Min/MaxValue instead of throwing
  • Applied to LastUpdatedToResourceSurrogateIdRewriter, DateTimeBoundedRangeRewriter, ScalarTemporalEqualityRewriter, and SearchValueExpressionBuilderHelper (ap comparator)

Related issues

Addresses [issue #181592].

Bug 181592: [Edge case] : 500 returned due to ArgumentOutOfRangeException for datetime

Testing

Tested by adding some UTs.

FHIR Team Checklist

  • Update the title of the PR to be succinct and less than 65 characters
  • Add a milestone to the PR for the sprint that it is merged (i.e. add S47)
  • Tag the PR with the type of update: Bug, Build, Dependencies, Enhancement, New-Feature or Documentation
  • Tag the PR with Open source, Azure API for FHIR (CosmosDB or common code) or Azure Healthcare APIs (SQL or common code) to specify where this change is intended to be released.
  • Tag the PR with Schema Version backward compatible or Schema Version backward incompatible or Schema Version unchanged if this adds or updates Sql script which is/is not backward compatible with the code.
  • When changing or adding behavior, if your code modifies the system design or changes design assumptions, please create and include an ADR.
  • CI is green before merge Build Status
  • Review squash-merge requirements

Semver Change (docs)

Patch|Skip|Feature|Breaking (reason)

@v-isyamauchi-gh v-isyamauchi-gh added this to the FY27\Q1\2wk\2wk05 milestone Sep 1, 2026
@v-isyamauchi-gh
v-isyamauchi-gh requested a review from a team as a code owner September 1, 2026 21:38
@v-isyamauchi-gh v-isyamauchi-gh added Bug Bug bug bug. No-Issue-Activity This issue is now considered stale and will be closed soon Azure API for FHIR Label denotes that the issue or PR is relevant to the Azure API for FHIR Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs No-PaaS-breaking-change No-ADR ADR not needed labels Sep 1, 2026
@v-isyamauchi-gh
v-isyamauchi-gh requested a lite review from Copilot September 1, 2026 21:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new safe arithmetic helpers and one call site can still produce incorrect results or overflow in edge cases (notably long.MinValue guard overflow and boundary-day semantics).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR prevents DateTime/DateTimeOffset arithmetic overflows in the FHIR search expression rewriting pipeline (notably SQL Server visitors and “ap” date comparator handling) by introducing saturating date arithmetic helpers and applying them to existing rewrite/optimization code paths.

Changes:

  • Added DateTimeSafeExtensions (SafeAddTicks / SafeAddDays) to clamp to min/max instead of throwing on overflow.
  • Updated SQL Server search expression visitors to use the safe arithmetic helpers in rewrite logic.
  • Updated Core search expression builder logic for ap date comparator to use safe tick arithmetic, and added unit tests for the new extension methods.
File summaries
File Description
src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/ScalarTemporalEqualityRewriter.cs Uses safe datetime arithmetic in precision classification for temporal equality rewrites.
src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/LastUpdatedToResourceSurrogateIdRewriter.cs Uses safe tick arithmetic when shifting millisecond-truncated _lastUpdated bounds into surrogate id space.
src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/DateTimeBoundedRangeRewriter.cs Uses safe tick arithmetic when generating bounded datetime range optimizations.
src/Microsoft.Health.Fhir.Core/Features/Search/Expressions/Parsers/SearchValueExpressionBuilderHelper.cs Uses safe tick arithmetic for “ap” comparator approximate range generation.
src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs New saturating arithmetic helpers for DateTime / DateTimeOffset.
src/Microsoft.Health.Fhir.Core.UnitTests/Extensions/DateTimeSafeExtensionsTests.cs New unit tests covering common and near-boundary behaviors of safe arithmetic helpers.
Review details

Suppressed comments (2)

src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs:51

  • The DateTimeOffset SafeAddTicks guard has the same long.MinValue overflow issue (MinValue.Ticks - ticks overflows when ticks == long.MinValue), which can throw before clamping.
            if (ticks < 0 && value.Ticks < DateTimeOffset.MinValue.Ticks - ticks)
            {
                return DateTimeOffset.MinValue;
            }

src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs:63

  • SafeAddDays computes ticks via days * TimeSpan.TicksPerDay, but that multiplication can overflow long for large |days|, producing an incorrect wrapped tick count and defeating the clamping behavior.
        public static DateTime SafeAddDays(this DateTime value, int days)
        {
            long ticks = days * TimeSpan.TicksPerDay;
            return value.SafeAddTicks(ticks);
        }
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new “safe” DateTime helpers currently have correctness gaps (e.g., clamping loses DateTime.Kind and SafeAddDays can overflow via unchecked multiplication) that can still lead to incorrect behavior in the rewritten search pipeline.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

src/Microsoft.Health.Fhir.Core.UnitTests/Extensions/DateTimeSafeExtensionsTests.cs:10

  • The using for Microsoft.Health.Test.Utilities appears unused in this test file and may trigger CS8019 (unnecessary using directive). Remove it to keep the test project warning-clean.

src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs:75

  • SafeAddDays multiplies days * TimeSpan.TicksPerDay, which can overflow long for large |days| values (unchecked arithmetic) and produce an incorrect wrapped tick count. Since these helpers are explicitly meant to be overflow-safe, compute the clamp using division (no multiplication overflow) before converting days→ticks.
        public static DateTime SafeAddDays(this DateTime value, int days)
        {
            long ticks = days * TimeSpan.TicksPerDay;
            return value.SafeAddTicks(ticks);
        }
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new DateTimeOffset safe helpers/documentation/tests have inconsistencies and edge cases where overflow handling can still throw (notably with non-zero/negative offsets), undermining the PR’s goal.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs:95

  • SafeAddDays(DateTimeOffset) can throw for negative offsets when days overflows (e.g., int.MaxValue) because new DateTimeOffset(DateTimeOffset.MaxValue.Ticks, value.Offset) is not representable for offsets < 0 (it would push the implied UTC time past DateTime.MaxValue). This reintroduces the overflow exceptions this PR is trying to eliminate.
            if (days > MaxDaysBeforeTicksOverflow || days < -MaxDaysBeforeTicksOverflow)
            {
                return days > 0 ? new DateTimeOffset(DateTimeOffset.MaxValue.Ticks, value.Offset) : new DateTimeOffset(DateTimeOffset.MinValue.Ticks, value.Offset);
            }

src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs:88

  • The XML doc for SafeAddDays(DateTimeOffset) claims it clamps to DateTimeOffset.MinValue/MaxValue, but the current implementation returns a value with the original offset (which is not equal to DateTimeOffset.MinValue/MaxValue unless the offset is zero). Update the doc comment to match the actual behavior.
        /// <summary>
        /// Adds the specified number of days to a <see cref="DateTimeOffset"/>, clamping the result
        /// to <see cref="DateTimeOffset.MinValue"/> or <see cref="DateTimeOffset.MaxValue"/> on overflow.
        /// </summary>
  • Files reviewed: 6/6 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs
Comment thread src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

DateTimeOffset clamping can still throw for non-zero offsets and one added unit test does not correctly validate kind preservation (and may introduce an unused-variable warning).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs:105

  • SafeAddDays(DateTimeOffset, int) has the same non-zero-offset problem as SafeAddTicks when it clamps: constructing new DateTimeOffset(DateTimeOffset.MinValue/MaxValue.Ticks, value.Offset) may itself throw. It’s safer to route the saturation case through SafeAddTicks so the offset-specific representable bounds are applied consistently.
            // Detect if days * TimeSpan.TicksPerDay would overflow long.
            if (days > MaxDaysBeforeTicksOverflow || days < -MaxDaysBeforeTicksOverflow)
            {
                return days > 0 ? new DateTimeOffset(DateTimeOffset.MaxValue.Ticks, value.Offset) : new DateTimeOffset(DateTimeOffset.MinValue.Ticks, value.Offset);
            }

src/Microsoft.Health.Fhir.Core.UnitTests/Extensions/DateTimeSafeExtensionsTests.cs:79

  • This test doesn’t actually verify kind preservation: the dt local is unused and the assertion expects Unspecified because it’s using DateTime.MaxValue (which is Unspecified) as the input. This can also introduce an unused-variable warning depending on build settings.
        public void GivenUtcDateTime_WhenSafeAddTicksClampsToMaxValue_ThenPreservesKind()
        {
            var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            DateTime result = DateTime.MaxValue.AddTicks(-1).SafeAddTicks(TimeSpan.TicksPerDay);
            Assert.Equal(DateTimeKind.Unspecified, result.Kind);

src/Microsoft.Health.Fhir.Core.UnitTests/Extensions/DateTimeSafeExtensionsTests.cs:132

  • The implementation is intended to be safe for extreme values; add offset edge-case tests that would have thrown previously (underflow at +offset and overflow at -offset) to prevent regressions, especially since the clamping logic depends on UTC/local bound interactions.
        [Fact]
        public void GivenDateTimeOffset_WhenSafeAddTicksClampsWithNonZeroOffset_ThenPreservesOffset()
        {
            var offset = TimeSpan.FromHours(5);
            var dto = new DateTimeOffset(9999, 12, 31, 23, 59, 59, offset);
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

_lastUpdated rewriting can still throw when converting large dates to surrogate IDs (ToSurrogateId enforces <= IdHelper.MaxDateTime), so some extreme-date queries may still result in 500s.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines 56 to +60
case BinaryOperator.GreaterThan:
return Expression.GreaterThanOrEqual(
SqlFieldName.ResourceSurrogateId,
null,
new DateTimeOffset(truncated.AddTicks(TimeSpan.TicksPerMillisecond)).ToSurrogateId());
new DateTimeOffset(truncated.SafeAddTicks(TimeSpan.TicksPerMillisecond)).ToSurrogateId());
Replace guard-based overflow/underflow detection with exception-based handling
in SafeAddTicks and SafeAddDays for both DateTime and DateTimeOffset.

The try-catch approach is more reliable because:
- It delegates validation to .NET's trusted implementation
- It catches any exception that arithmetic throws (including edge cases)
- Guard conditions cannot cover all edge cases comprehensively

SafeAddDays still detects multiplication overflow upfront to avoid
calculating invalid tick values before passing to SafeAddTicks.

DateTimeOffset clamping remains offset-aware to preserve the offset
when returning a clamped value.

All 28 existing tests pass with this approach.

Fixes: PR #5781

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The PR includes a machine-specific SQL Server connection string in launchSettings.json that should be reverted to a repo-friendly default.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Microsoft.Health.Fhir.R4.Web/Properties/launchSettings.json Outdated
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.95%. Comparing base (20d379f) to head (a954c15).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #5781      +/-   ##
==========================================
+ Coverage   78.28%   78.95%   +0.66%     
==========================================
  Files        1016     1017       +1     
  Lines       36940    36974      +34     
  Branches     5619     5633      +14     
==========================================
+ Hits        28920    29193     +273     
+ Misses       6644     6400     -244     
- Partials     1376     1381       +5     

see 14 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The _lastUpdated surrogate-id rewrite can change strict comparison semantics at the max representable boundary when clamping occurs, and should explicitly handle the “clamped to max” case.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/LastUpdatedToResourceSurrogateIdRewriter.cs:60

  • When the "next millisecond" calculation overflows (e.g., original value is at/near DateTimeOffset.MaxValue), SafeAddTicks clamps to DateTime.MaxValue. In that case this rewrite still uses GreaterThanOrEqual, which changes strict ">" semantics into ">= max" and could incorrectly match a boundary row if one ever exists at the max surrogate id. Consider detecting the clamp and emitting a strict GreaterThan in the overflow case.

This issue also appears on line 81 of the same file.

src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/LastUpdatedToResourceSurrogateIdRewriter.cs:85

  • When the "next millisecond" calculation overflows and clamps to DateTime.MaxValue, rewriting "<=" as "< next" becomes "< max", which is no longer equivalent to "<= original" at the representable upper bound (it would exclude a row exactly at max surrogate id). In the clamp case, use LessThanOrEqual against the clamped surrogate id instead of LessThan.
                case BinaryOperator.LessThanOrEqual:
                    return Expression.LessThan(
                        SqlFieldName.ResourceSurrogateId,
                        null,
                        new DateTimeOffset(truncated.SafeAddTicks(TimeSpan.TicksPerMillisecond)).ToSurrogateId());
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes are narrowly scoped, consistently applied to the known overflow points, and backed by targeted unit tests for key edge cases.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

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

Labels

Azure API for FHIR Label denotes that the issue or PR is relevant to the Azure API for FHIR Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs Bug Bug bug bug. No-ADR ADR not needed No-Issue-Activity This issue is now considered stale and will be closed soon No-PaaS-breaking-change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants