Skip to content

fix(events): catch FileNotFoundException in openBufferedReader to prevent TOCTOU crash#3704

Merged
tonidero merged 1 commit into
RevenueCat:external/tsushanth/fix/filereader-toctou-fnfefrom
tsushanth:fix/filereader-toctou-fnfe
Jul 6, 2026
Merged

fix(events): catch FileNotFoundException in openBufferedReader to prevent TOCTOU crash#3704
tonidero merged 1 commit into
RevenueCat:external/tsushanth/fix/filereader-toctou-fnfefrom
tsushanth:fix/filereader-toctou-fnfe

Conversation

@tsushanth

@tsushanth tsushanth commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Fixes #3696.

EventsFileHelper.readFile guards with fileIsEmpty() before opening the event store file, but FileHelper.openBufferedReader then calls FileInputStream(file) without re-checking existence. A concurrent flush/rotate that deletes ad_event_store.jsonl between fileIsEmpty() returning false and FileInputStream(file) throws an uncaught FileNotFoundException on the SDK background thread (PurchasesFactory$LowPriorityThreadFactory), crashing the app.

From the reporter: this is the #1 fatal crash on production builds using AdMob/AppLovin ad trackers, with ~53 crashes / ~52 affected users over 30 days, reproduced on Android 14–16.

Crash stack:

Fatal Exception: java.io.FileNotFoundException: .../event_store/ad_event_store.jsonl: open failed: ENOENT
    at FileHelper.openBufferedReader(FileHelper.kt:80)
    at FileHelper.readFilePerLines(FileHelper.kt:39)
    at EventsFileHelper.readFile(EventsFileHelper.kt:61)
    at EventsManager.getStoredEvents(EventsManager.kt:422)
    at EventsManager.flushNextBatch(EventsManager.kt:277)

Fix

Catch FileNotFoundException in openBufferedReader and treat a missing file as empty — the same semantics fileIsEmpty() uses — logging at DEBUG level. The same pattern is already used in removeFirstLinesFromFile (line 58).

} catch (e: FileNotFoundException) {
    debugLog { "FileHelper: file not found when trying to read: $filePath. Treating as empty." }
}

Also adds a regression test that calls readFilePerLines on a nonexistent path and asserts no exception propagates and the block receives an empty sequence.

Testing

  • New unit test in FileHelperTest covers the missing-file path.
  • Existing tests all still pass.

Note

Low Risk
Narrow defensive change in file I/O with empty-file semantics preserved; low blast radius beyond preventing a known production crash path.

Overview
Fixes a TOCTOU race where ad/event store reads could crash the app: callers check fileIsEmpty() then readFilePerLines, but a concurrent flush/rotate can delete the file before FileInputStream opens, yielding an uncaught FileNotFoundException on the SDK background thread.

FileHelper.openBufferedReader now catches FileNotFoundException, logs at debug, and treats a missing file as empty (matching fileIsEmpty()), aligning with the existing pattern in removeFirstLinesFromFile.

A regression test asserts readFilePerLines on a nonexistent path does not throw and leaves the consumer with no lines.

Reviewed by Cursor Bugbot for commit 801a0b5. Bugbot is set up for automated code reviews on this repo. Configure here.

…vent TOCTOU crash

EventsFileHelper.readFile guards with fileIsEmpty() before opening the event store
file, but FileHelper.openBufferedReader opens it with FileInputStream(file) without
re-checking existence. A concurrent flush/rotate that deletes ad_event_store.jsonl
between the fileIsEmpty() check and the FileInputStream call throws an uncaught
FileNotFoundException on the SDK background thread, killing the process. This is
the RevenueCat#1 fatal crash on production builds that use AdMob/AppLovin ad trackers.

Fix: catch FileNotFoundException in openBufferedReader and treat a missing file as
empty — the same semantics fileIsEmpty() uses — logging at DEBUG level. The same
pattern is already used in removeFirstLinesFromFile.

Fixes RevenueCat#3696
@tsushanth tsushanth requested a review from a team as a code owner July 3, 2026 01:01

@tonidero tonidero 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.

Thank you so much for your contribution @tsushanth! This looks great! Just a very small nitpick comment and it should be good to get merged. Thank you again!

// fileIsEmpty() before calling here, but a concurrent flush/rotate can delete
// the file between that check and FileInputStream(file), producing an uncaught
// FileNotFoundException on the SDK background thread that crashes the process.
// Treat a missing file as empty — the same semantics fileIsEmpty() uses.

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.

I believe we don't really need this comment, since the code is self-explanatory. Can you remove it?

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.

Actually, we can handle it on our side on the merge PR :) We will get this merged now. Thanks again!

@tonidero tonidero changed the base branch from main to external/tsushanth/fix/filereader-toctou-fnfe July 6, 2026 08:23
@tonidero tonidero merged commit b71efbf into RevenueCat:external/tsushanth/fix/filereader-toctou-fnfe Jul 6, 2026
3 checks passed
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.

Crash: FileHelper.openBufferedReader (adTracker)

2 participants