Skip to content

Fixing datetime arithmetic overflow/underflow. - #5781

Merged
v-isyamauchi-gh merged 28 commits into
mainfrom
personal/v-isyamauchi/181592
Sep 11, 2026
Merged

Fixing datetime arithmetic overflow/underflow.#5781
v-isyamauchi-gh merged 28 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 Outdated

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 Outdated

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 Outdated
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 Outdated

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

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

One critical and two moderate unresolved findings remain in datetime rewrite logic.

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

Review details

Suppressed comments (2)

src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/DateTimeBoundedRangeRewriter.cs:45

  • When the left predicate is GreaterThan and its value is within one day of DateTimeOffset.MinValue, this clamp changes the derived optimization's meaning: the mathematical bound X - 1 day is below the representable range, so every representable DateTimeStart (including DateTime.MinValue) satisfies it, but DateTimeStart > DateTime.MinValue excludes the exact-minimum row. Detect this underflow and make the bound inclusive, or skip the optimization for that case to avoid false negatives.
                    new BinaryExpression(left.BinaryOperator, FieldName.DateTimeStart, left.ComponentIndex, ((DateTimeOffset)left.Value).SafeAddTicks(-TimeSpan.TicksPerDay)),

src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/ScalarTemporalEqualityRewriter.cs:137

  • For birthdate=9999-12-31, start.SafeAddDays(1) saturates to DateTimeOffset.MaxValue, and subtracting one tick yields MaxValue - 1 tick; however, DateTimeSearchValue.End for that date is MaxValue. The exact-day value therefore fails Precision.ExactDay and misses the end-only SQL optimization. Preserve the last representable day's true inclusive end when the day increment saturates.
            if (end == start.SafeAddDays(1).SafeAddTicks(-1))
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Search/DateSearchTests.cs Dismissed

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

Unresolved critical test compilation errors and a moderate underflow-boundary rewrite bug remain.

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

Review details

Suppressed comments (6)

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

src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/DateTimeBoundedRangeRewriter.cs:45

  • When the matched left predicate is DateTimeEnd > X and X is within one day of the minimum representable date, this clamp turns X - 1 day into the minimum value but keeps the strict > operator. The optimized branch therefore emits DateTimeStart > DateTime.MinValue and drops valid short ranges whose start is exactly the minimum; adjust the bound/operator on underflow or skip this optimization at that boundary.

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

  • ILogger.Log<TState> is generic, but these default literals provide no type from which the compiler can infer TState, so this assertion fails with CS0411. Specify the state type (for example, Log<object>(...)) as done by the existing logger tests.
            logger.ReceivedWithAnyArgs().Log(default, default, default, default, default!);

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

  • ILogger.Log<TState> is generic, but these default literals provide no type from which the compiler can infer TState, so this assertion fails with CS0411. Specify the state type (for example, Log<object>(...)) as done by the existing logger tests.
            logger.DidNotReceiveWithAnyArgs().Log(default, default, default, default, default!);

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

  • ILogger.Log<TState> is generic, but these default literals provide no type from which the compiler can infer TState, so this assertion fails with CS0411. Specify the state type (for example, Log<object>(...)) as done by the existing logger tests.
            logger.ReceivedWithAnyArgs().Log(default, default, default, default, default!);

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

  • ILogger.Log<TState> is generic, but these default literals provide no type from which the compiler can infer TState, so this assertion fails with CS0411. Specify the state type (for example, Log<object>(...)) as done by the existing logger tests.
            logger.ReceivedWithAnyArgs().Log(default, default, default, default, default!);

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

  • ILogger.Log<TState> is generic, but these default literals provide no type from which the compiler can infer TState, so this assertion fails with CS0411. Specify the state type (for example, Log<object>(...)) as done by the existing logger tests.
            logger.ReceivedWithAnyArgs().Log(default, default, default, default, default!);
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • 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.

🔵 Needs a closer look

Two moderate boundary-correctness findings, each with 2 votes, remain unresolved.

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/DateTimeBoundedRangeRewriter.cs:45

  • When left.BinaryOperator is GreaterThan and subtracting a day underflows, SafeAddTicks clamps the value to DateTimeOffset.MinValue but this line preserves the strict > operator. The derived optimization then requires DateTimeStart > MinValue; a valid short range starting exactly at MinValue can satisfy the original DateTimeEnd > X predicate but be filtered out by this branch. Detect the underflow and either omit this added bound or change it to >= MinValue, with a regression test for a gt search at the minimum date.

src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/ScalarTemporalEqualityRewriter.cs:137

  • For start = 9999-12-31T00:00:00Z, SafeAddDays(1) clamps to DateTimeOffset.MaxValue, so the chained SafeAddTicks(-1) computes MaxValue - 1 tick instead of the actual inclusive end of that day (MaxValue). Consequently the exact-day optimization does not recognize birthdate=9999-12-31 and can classify a non-full-day MaxValue - 1 tick endpoint as exact. Treat a clamped next day as the end directly rather than subtracting a tick after clamping.
            if (end == start.SafeAddDays(1).SafeAddTicks(-1))
  • Files reviewed: 8/8 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.

🔵 Needs a closer look

Add regression coverage for extreme future dates in the ap parser path.

Review details

Suppressed comments (1)

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

src/Microsoft.Health.Fhir.Core/Features/Search/Expressions/Parsers/SearchValueExpressionBuilderHelper.cs:106

  • The new overflow-safe calls in the ap path are not covered by a regression test: the existing GivenADateWithApComparator cases stop at year 2220, while the reported failure involves values such as ap9500-01-01 whose widened start bound exceeds DateTime.MaxValue. Add a parser/unit test that exercises an extreme future date and verifies the expression is built without throwing and the bounds are clamped.
  • Files reviewed: 10/10 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

Boundary-safe arithmetic and focused unit and end-to-end coverage address the reported overflow issue.

Review details
  • Files reviewed: 11/11 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

Boundary-safe arithmetic and supporting unit and E2E coverage are included with no unresolved blocking issues.

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

@v-isyamauchi-gh
v-isyamauchi-gh merged commit da355bc into main Sep 11, 2026
49 checks passed
@v-isyamauchi-gh
v-isyamauchi-gh deleted the personal/v-isyamauchi/181592 branch September 11, 2026 20:50
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.

5 participants