Skip to content

android: force Client.app to initialize before executing local API requests - #839

Open
pinoybear wants to merge 1 commit into
tailscale:mainfrom
pinoybear:fix/client-app-lazy-init-not-forced
Open

android: force Client.app to initialize before executing local API requests#839
pinoybear wants to merge 1 commit into
tailscale:mainfrom
pinoybear:fix/client-app-lazy-init-not-forced

Conversation

@pinoybear

Copy link
Copy Markdown

What this fixes

Fixes the lateinit property app has not been initialized crash at Request.execute(), most recently reported in tailscale/tailscale#20884, and previously in #14125 / #14314.

Root cause

Client has an app property intended to force App.get() (and therefore Request.setApp()) to run before any local API call:

private val app: libtailscale.Application by lazy { App.get().getLibtailscaleApp() }

This property was added in #563 specifically to prevent this crash ("Lazily init app in Client to ensure that we aren't trying to make any local API calls before app has been initialized"). However, by lazy only evaluates on first read of the property, and nothing in Client ever reads this.app — none of start(), status(), prefs(), etc. reference it. So the property has never actually forced initialization since it was introduced; it's been dead code from the start (confirmed by checking every commit that has touched this file since #563 — none add or remove a reference to app).

If a Client is constructed and used before anything else has called App.get() — e.g. a WorkManager background task running on a cold-started process — Request.execute() reads the companion object's separate lateinit var app (set only via Request.setApp(), called from App.startLibtailscale()) before it's been set, and crashes.

Fix

Add an init block that reads app, forcing the lazy initializer to run at Client construction time rather than never. App.initOnce() is @Synchronized, so this blocks the constructing thread until App.get() (and Request.setApp()) has fully completed, closing the race.

Client.kt is the only place Request is constructed in this codebase, so this covers every path that can reach this crash today.

This isn't a novel pattern — Notifier.start() already guards the equivalent case correctly:

if (!::app.isInitialized) {
  App.get()
}

This PR brings Client in line with that existing, correct approach used elsewhere in this codebase for the same class of problem.

Verification

Static/manual verification only — I don't have a full Android build environment to run make fmt-check / make tailscale-debug.apk locally, so please treat CI as the source of truth for formatting and build correctness. Happy to address any CI failures.

Confirmed via adb shell dumpsys dropbox that this exact crash signature is currently reproducing on a shipped 1.102.2 build (Galaxy Z Fold8, and previously a Fold7 on a different Android major version), despite the crash being reported "resolved" after #563/#14125. Full crash details and device history in tailscale/tailscale#20884.

@bradfitz

Copy link
Copy Markdown
Member

I don't have a full Android build environment

Please elaborate. Did you compile & test this change on your own device or not?

…quests

Client's `app` property was added in tailscale#563 to force App.get() (and
therefore Request.setApp()) to run before any local API call, but
`by lazy` only evaluates on first read, and nothing in Client ever
reads `this.app`. The property has been dead code since it was
introduced, so this never actually prevented the race it was meant to
prevent.

If a Client is constructed and used before anything else has called
App.get() -- e.g. a WorkManager background task on a cold-started
process -- Request.execute() reads the companion object's separate
lateinit var app before Request.setApp() has run, and crashes with
"lateinit property app has not been initialized".

Add an init block that reads app, forcing the lazy initializer at
construction time. App.initOnce() is @synchronized, so this blocks
the constructing thread until initialization has completed. This
brings Client in line with Notifier.start(), which already guards
the equivalent case correctly.

Fixes tailscale/tailscale#20884
Updates tailscale/tailscale#14125
Updates tailscale/tailscale#14314

Signed-off-by: pinoybear <22431440+pinoybear@users.noreply.github.com>
@pinoybear
pinoybear force-pushed the fix/client-app-lazy-init-not-forced branch from d49c0c0 to ab045c4 Compare August 15, 2026 17:58
@pinoybear

Copy link
Copy Markdown
Author

Correct — I have not compiled or run this on a device. This was static analysis: full rename-aware commit history of Client.kt (via local git log --follow, not just the GitHub API, which under-reports by one commit), a full-repo grep confirming Client.kt is the only place Request is constructed, and a byte-for-byte diff against the exact commit that produced the shipped 1.102.2-...-g0b3c1bdb2 build to make sure I was reasoning about the actual code in question.

The fix itself is a one-line addition (init { app }) using a pattern that already exists and works correctly elsewhere in this codebase (Notifier.start()'s if (!::app.isInitialized) { App.get() } guard), so I'm confident in the mechanism, but I have not verified it builds or passes make fmt-check locally — happy to have CI be the judge, or if you'd rather I build and test it on-device first before this goes further, let me know and I'll do that.

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