Skip to content

fix: discard corrupt cache files instead of looping on them (resulting in an OOM exception) - #5507

Merged
jamescrosswell merged 6 commits into
getsentry:mainfrom
lgarczyn:cache-poison-oom-loop
Sep 4, 2026
Merged

fix: discard corrupt cache files instead of looping on them (resulting in an OOM exception)#5507
jamescrosswell merged 6 commits into
getsentry:mainfrom
lgarczyn:cache-poison-oom-loop

Conversation

@lgarczyn

@lgarczyn lgarczyn commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Header read caps at 64 KB, and the discard log no longer reads the whole file.

One mac dev was stuck at 300+Gb usage because sentry was trying to read aarge corrupted dump

test: cover the corrupt cache discard through SentrySdk.Init

Closes #5510

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Aug 25, 2026
@jamescrosswell

jamescrosswell commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Hi @lgarczyn - thanks for the contribution.

Could I get a bit of context for this? I don't think any issue was raised. What circumstances were you running into problems? What happens vs what you expect to happen? Is there an easy way to reproduce this?

Thanks in advance.

@lgarczyn

Copy link
Copy Markdown
Contributor Author

Hi @lgarczyn - thanks for the contribution.

Could I get a bit of context for this? I don't think any issue was raised. What circumstances were you running into problems? What happens vs what you expect to happen? Is there an easy way to reproduce this?

Thanks in advance.

Hello!

One of our designer's mac computer crashed.

When it restarted, it was extremely sluggish, with insane memory usage.

Trying to debug it, we found out sentry was trying to load a giant log or dmp, failing, and then just trying again.

This is to try and mitigate it

@lgarczyn

Copy link
Copy Markdown
Contributor Author

A crash mid-write leaves a big NUL-filled envelope in the cache.

ReadLineAsync has no length cap, so the header read OOMs.

OutOfMemoryException isn't JsonException, so the discard catch never fires

MoveUnprocessedFilesBackToCache starts the loop again the file every launch.

Manual fix: Deleting the cache by hand.

This PR: Cap the header read at 64 KB, route InvalidDataException through the existing discard, and limit LogFailureWithDiscard, so it doesn't try to pickup 100Gb file.

Test: cover the corrupt cache discard through SentrySdk.Init. i didn't try to reproduce an actual OOM, because, tbh, I'm not sure how'd you'd test that.

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.35294% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.70%. Comparing base (365a803) to head (a5291c4).

Files with missing lines Patch % Lines
src/Sentry/Internal/Http/CachingTransport.cs 79.31% 5 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5507      +/-   ##
==========================================
- Coverage   74.71%   74.70%   -0.01%     
==========================================
  Files         515      515              
  Lines       18930    18948      +18     
  Branches     3692     3696       +4     
==========================================
+ Hits        14143    14155      +12     
- Misses       3905     3909       +4     
- Partials      882      884       +2     

☔ 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.

@jamescrosswell jamescrosswell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @lgarczyn - that makes sense. I've raised #5510 to capture the key context.

Generally your change looks good. I made a couple of suggestions to your code.

Additionally, it might be nice to address a similar issue in EnvelopeItem.DeserializePayloadAsync... this takes length straight from the item header and does (int)(payloadLength ?? stream.Length). A header with a bogus length gives either an unchecked overflow or an OOM, and a large file with no length key at all overflows on (int)stream.Length.

That one is maybe a bit trickier... Ideally we'd validate the length against the remaining stream length and throw InvalidDataException if there was an inconsistency... so something like:

if (payloadLength is > int.MaxValue or < 0)
{
    throw new InvalidDataException($"Envelope item length {payloadLength} is not a valid buffer size.");
}
var remaining = stream.Length - stream.Position;
if (payloadLength > remaining)
{
    throw new InvalidDataException($"Envelope item claims {payloadLength} bytes but only {remaining} remain.");
}

That should work for us since we ensure CanSeek for the stream... meaning we shouldn't get NotSupportedException on Stream.Length.

Comment thread src/Sentry/Internal/Extensions/StreamExtensions.cs Outdated
Comment thread src/Sentry/Internal/Http/CachingTransport.cs Outdated
}
}

// Only corrupt files get here and they can be huge, so don't read the whole thing

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Only corrupt files get here and they can be huge, so don't read the whole thing
/// <summary>
/// Only corrupt files get here and they can be huge, so don't read the whole thing
/// </summary>

Just for consistency... we usually XML comment methods in the repo (even if they're private).

It's a good change though 👍🏻

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These are all very good feedback, I'll get around to it at some point. For now, I just wanted our death loop fixed ^^

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'll get around to it at some point. For now, I just wanted our death loop fixed

If you don't have time to implement those changes, I can do it from my side...

I agree we should get this out as soon as possible but worth making the changes now before a merge - it's very hard to circle back on things otherwise and we just accumulate tech debt.

jamescrosswell and others added 3 commits September 4, 2026 11:20
A fixed MaxLineLength on StreamExtensions baked "this is only ever used to read
envelope headers, and those are small" into a general-purpose stream helper. The
knowledge of what a reasonable length looks like belongs with the callers, and
keeping it there means adding a field to a header can't silently invalidate an
assumption living in an unrelated file.

ReadLineAsync now takes an optional maxLength and only enforces a cap when one is
supplied. Envelope and EnvelopeItem each declare their own limit next to a comment
describing what that particular header actually contains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Catching a whitelist of exception types is how this bug happened: the runaway
header read surfaces as OutOfMemoryException, or as IOException("Stream was too
long") once the buffer passes the 2 GB array limit, and neither is a JsonException,
so the file was never discarded and came back on every launch. Adding
InvalidDataException to the list fixes the case we hit but leaves us guessing at
what a parser can throw when handed arbitrary corrupted bytes.

Scope the try to the deserialize call instead and discard on anything other than
OperationCanceledException. A file we cannot deserialize is a file we can never
send, whatever the exception type. We may throw away the odd envelope on a
transient read error, which beats a corrupt file stalling the cache indefinitely.

The try deliberately covers only Envelope.DeserializeAsync rather than widening the
existing one, which also wrapped the send block. Those catches rethrow on
cancellation and on network-unavailable errors so the worker retries the file
later; swallowing them would delete every envelope buffered during an outage.
Narrowing that outer catch instead is no better, because IsNetworkUnavailableError
matches bare IOException and would classify the corrupt-file read as a network blip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread src/Sentry/Internal/Http/CachingTransport.cs Outdated
Comment thread src/Sentry/Protocol/Envelopes/EnvelopeItem.cs Outdated
@jamescrosswell
jamescrosswell self-requested a review September 4, 2026 06:30
Comment thread src/Sentry/Protocol/Envelopes/Envelope.cs Outdated
Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>

@jamescrosswell jamescrosswell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@lgarczyn I merged in the changes we discussed (figured you might not easily find the time). Thank you very much for the contribution!

@jamescrosswell
jamescrosswell merged commit 4746ed8 into getsentry:main Sep 4, 2026
5 of 6 checks passed
@lgarczyn
lgarczyn deleted the cache-poison-oom-loop branch September 4, 2026 08:33
@lgarczyn

lgarczyn commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

@lgarczyn I merged in the changes we discussed (figured you might not easily find the time). Thank you very much for the contribution!

Thank you so much!

@jamescrosswell jamescrosswell changed the title fix: discard corrupt cache files instead of OOM looping on them fix: discard corrupt cache files instead of looping on them (resulting in an OOM exception) Sep 10, 2026
This was referenced Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Corrupt file in the offline cache leads to OOM exceptions

2 participants