Sitelet https://github.com/flutter/flutter/pull/189145
Skip to content

Prefer OS-installed signed APKs over app-writable .so files when loading deferred libraries - #189145

Merged
auto-submit[bot] merged 17 commits into
flutter:masterfrom
gmackall:deferred-component-prefer-signed-apks
Aug 14, 2026
Merged

auto-submit[bot] merged 17 commits into
flutter:masterfrom
gmackall:deferred-component-prefer-signed-apks

Conversation

@gmackall

@gmackall gmackall commented Jul 8, 2026 •

Copy link
Copy Markdown
Member

LoadDartDeferredLibrary in platform_view_android_jni_impl.cc walked its
search paths back to front (search_paths.back() / pop_back()), which is
the opposite of the contract documented in three places:

  • FlutterJNI.loadDartDeferredLibrary: "Paths will be tried first to last and
    ends when a library is successfully found."
  • DeferredComponentManager.loadDartLibrary: "Each search path will be
    attempted in order until a shared library is found."
  • The comment in PlayStoreDeferredComponentManager reading
    // Add the bare filename as the first search path — which was in fact tried
    last.

PlayStoreDeferredComponentManager builds the list as
[bare filename, signed split APKs..., loose .so files from getFilesDir()...],
so reversing it meant the app-writable getFilesDir() copies were consulted
first and the OS-installed signed splits only as a fallback, which is exactly
backwards from the intended trust ordering.

This PR makes the native side honor the documented order. The Java-side list
construction is unchanged; only comments were added there to record why the
ordering matters.

gmackall added 6 commits July 8, 2026 10:37
…omponent libs

the native loader dlopens search paths from the end of the list first, but
we were appending the app-writable getFilesDir .so paths last, so a loose .so
in internal storage would win over the os-installed signed apk. flip the order
so the signed apks are tried first and getFilesDir is only a fallback.
turns out the native loader iterating back to front is the actual bug: it
contradicts the documented 'first to last' contract on
FlutterJNI.loadDartDeferredLibrary. so fix it there (iterate front to back)
rather than compensating in java, which fixes it for any custom manager too.
revert PlayStoreDeferredComponentManager back to natural most-preferred-first
order (bare name, signed apks, then getFilesDir fallback last).

only changes behavior when multiple sources are loadable at once, so normal
single-source installs and the getFilesDir-only download flow are unaffected.
pull the dlopen loop out of LoadDartDeferredLibrary into a named helper
declared in the header, so the ordering is unit-testable without making the
impl test-aware (no injected/fake dlopen). tests cover empty and no-match
returning null, and -- using two distinct system libs as loadable stand-ins,
so no build-time .so fixtures are needed -- that the earliest loadable path
wins.
the system-lib version was fragile: platform ifdefs plus relying on
libc/libm/libz being present and distinct on the runner. the property we
actually care about is just the ordering (first-to-last, stop at first
success), so add a testable overload of FindFirstLoadableLibrary that takes
the loader as a param and assert order + short-circuit with a fake. production
keeps the 1-arg signature and supplies dlopen. real load-path fidelity stays
where it belongs, in deferred_components_test.
string/vector were already used transitively by the existing method sigs in
this header; only functional (for std::function) is genuinely new.
drop the 1-arg overload; the sole caller in LoadDartDeferredLibrary passes
the dlopen wrapper directly. one signature, loader injected, tests unchanged.
@github-actions github-actions Bot added platform-android Android applications specifically engine flutter/engine related. See also e: labels. team-android Owned by Android platform team labels Jul 8, 2026
@gmackall gmackall added the CICD Run CI/CD label Jul 8, 2026
@gmackall gmackall added CICD Run CI/CD and removed CICD Run CI/CD labels Jul 15, 2026
gmackall and others added 5 commits July 27, 2026 10:54
reverts the earlier "these come in transitively" call. the transitive
includes do work, but include-what-you-use is the rule and the header
names both types in its own declarations.
junit is (expected, actual), and every other test in the file follows
that. also joins the signature back onto one line to match
google-java-format.
the doc said it returns the first entry of search_paths, but it returns
the handle for that entry.
@gmackall gmackall changed the title Fix loading order of dart deferred libraries in platform_view_android_jni_impl Prefer OS-installed signed APKs over app-writable .so files when loading deferred libraries Aug 13, 2026
@gmackall
gmackall marked this pull request as ready for review August 13, 2026 23:54
@gmackall
gmackall requested a review from a team as a code owner August 13, 2026 23:54
@gmackall
gmackall requested review from jesswrd and mboetger and removed request for a team August 13, 2026 23:54

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request refactors the deferred component library loading logic to enforce and document search path priorities, ensuring that OS-installed signed APKs are preferred over loose .so files in app-writable internal storage for security. It introduces a helper function FindFirstLoadableLibrary in C++ to load libraries in descending priority order, and adds corresponding unit tests in both C++ and Java to verify this ordering behavior. There are no review comments, and I have no feedback to provide.


// These paths are handed to the native loader in descending priority order: it tries them
// first to last and stops at the first that loads (see FlutterJNI.loadDartDeferredLibrary).
// Ordering matters for security -- OS-installed signed APKs must be preferred over loose .so

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.

"Over loose .so"?
the phrase is used in many places in this pr but I am not sure the concept is well defined. Especially when reading this code outside of this pr.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In this context I mean "Loose .so" as in sitting in the top level of the dir, as a standalone file, as opposed to say:


where it is inside an apk archive. I.e. "unbundled", or similar. But I can update it to say that more explicitly

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

updated to have that wording

doReturn(null).when(spyContext).getAssets();

// A loose .so sitting in the app-writable internal storage dir (getFilesDir()).
String soTestPath = "test/path/libapp.so-123.part.so";

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.

for my own understanding why are non signed so files in app writable storage allowed as a load path at all?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't know why they are, we could consider investigating/deprecating if not needed. It would be an independent change though

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I made an issue
#191139

}

@Test
public void searchPathsPrioritizeSignedApksOverInternalStorageSo() throws NameNotFoundException {

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 find this test hard to read.

There are somethings that are odd like assertEquals(1, jni.loadDartDeferredLibraryCalled); why is something "called" being compared to a number.

There is also some string duplication around "libapp.so-123.part.so" which itself is kind of a hard to read/parse file to look for.

Overall I would ask if you could try to make this test easier to understand for someone debugging it later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm I'd prefer to keep this test in line stylistically with the others in the file. jni.loadDartDeferredLibraryCalled is an integer invocation counter defined on the existing TestFlutterJNI mock in this test suite, which is already used in all the other tests. Similarly the other tests in this file, and most of the java tests in the embedder, follow the pattern of duplicating strings per test to make them self contained. I could change if you feel strongly, but I actually prefer it that way because of the different highlighting I get in IDE for strings vs variables which I find much more readable in a wall of asserts:

Screenshot 2026-08-14 at 12 24 38 PM

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.

ok I still find the test hard to read but this change is not the place to change the style of jni testing.

@reidbaker

Copy link
Copy Markdown
Contributor

How do we need to communicate this change to our users. In theory there could be developers who will get different native code loaded.

@gmackall

gmackall commented Aug 14, 2026 •

Copy link
Copy Markdown
Member Author

How do we need to communicate this change to our users. In theory there could be developers who will get different native code loaded.

Yes, there could be. It would only break those who are assuming the opposite of the public contract though. I think we should add a note to the flutter release blog that we fixed a bug in loading order of native libraries when using deferred components

@gmackall gmackall added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 14, 2026
@reidbaker

Copy link
Copy Markdown
Contributor

How do we need to communicate this change to our users. In theory there could be developers who will get different native code loaded.

Yes, there could be. It would only break those who are assuming the opposite of the public contract though. I think we should add a note to the flutter release blog that we fixed a bug in loading order of native libraries when using deferred components

I mean they are not assuming the opposite of the public contract. They are assuming the behavior as observed especially if there is a reason they have multiple so files and want to find one before another.

That said this is a security problem and we are doing the opposite of our public contract. I just want to make sure that if we break users they have a way of finding out what changed.

@gmackall

Copy link
Copy Markdown
Member Author

How do we need to communicate this change to our users. In theory there could be developers who will get different native code loaded.

Yes, there could be. It would only break those who are assuming the opposite of the public contract though. I think we should add a note to the flutter release blog that we fixed a bug in loading order of native libraries when using deferred components

I mean they are not assuming the opposite of the public contract. They are assuming the behavior as observed especially if there is a reason they have multiple so files and want to find one before another.

That said this is a security problem and we are doing the opposite of our public contract. I just want to make sure that if we break users they have a way of finding out what changed.

Yeah it's possible some are going based on observation. Regardless, I think we should add a note to our release, but as it's the opposite of the contract, and the contract behavior is more secure, I think we should land the fix

@gmackall

Copy link
Copy Markdown
Member Author

I think the vast vast majority won't notice a difference though - in either case a .so with the provided name is being loaded, and I think in most cases there is probably only one which matches

@gmackall gmackall added autosubmit Merge PR when tree becomes green via auto submit App and removed autosubmit Merge PR when tree becomes green via auto submit App labels Aug 14, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Aug 14, 2026
Merged via the queue into flutter:master with commit c41ae8c Aug 14, 2026
28 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-android Android applications specifically team-android Owned by Android platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants