| # How does loading WebView work? | |
| [TOC] | |
| ## Summary | |
| This is a technical explanation of how loading WebView into an Android app | |
| actually works, including details of how this varies across different versions | |
| of Android and the bugs and limitations that exist. This is important if making | |
| changes to how WebView is packaged, as we typically need to maintain backward | |
| compatibility. This may not be 100% complete as a lot of things have changed | |
| over the years that WebView has been updatable. This currently covers Android L | |
| and beyond, however Android L through Android P are no longer supported for | |
| WebView updates. | |
| ## General stuff | |
| The WebView implementation was moved out of the framework and into a separate | |
| APK in L. All the core framework code is automatically present in every app at | |
| runtime, but code in additional APKs and libraries is not, so this change | |
| required that we create a mechanism to load it automatically when the app uses | |
| WebView. | |
| *** aside | |
| In N, we made it possible for the Chrome APK to also provide a WebView | |
| implementation to save space, and introduced mechanisms to enable switching | |
| between different implementations. This is discussed in more detail in the | |
| [channel switching documentation](channels.md). For the purposes of this doc, | |
| you can assume all references to "the APK" or "the WebView APK" refer to the | |
| currently selected WebView implementation - loading is handled the same way. | |
| Switching WebView provider is also handled the same way as updating the current | |
| provider. | |
| *** | |
| The APK contains three categories of "stuff": compiled Java code (one or more | |
| `.dex` files), compiled native code (one or more `.so` files), and non-code data | |
| such as assets and resources. Each of these is discussed in its own section, but | |
| they are not completely independent of each other and are best read in order. | |
| The loading process begins when an application first instantiates one of the | |
| classes or calls one of the static methods in the framework's `android.webkit` | |
| package - most classes in that package require the WebView implementation to be | |
| loaded to be used. This includes inflating an XML layout that contains a | |
| WebView, since this constructs a WebView instance. | |
| The process is driven by the `WebViewFactory` class in the framework, and most | |
| of the code involved in loading the implementation is also part of the | |
| framework. This means that it can't be updated outside of the Android platform | |
| release cycle, somewhat limiting our flexibility as we have to remain backward | |
| compatible with old versions of the loading code. | |
| ## Getting a Context | |
| The Android platform provides a supported mechanism for applications to load and | |
| access the APKs of other installed apps: the | |
| [`Context.createPackageContext`](https://developer.android.com/reference/android/content/Context#createPackageContext%28java.lang.String,%20int%29) | |
| method, which returns a `Context` object for that app's APK. The WebView loading | |
| mechanism uses this on L and M. | |
| A side effect of `createPackageContext` is that `ActivityManager` is notified | |
| that the calling process has loaded the APK in question. This is used to decide | |
| which processes should be killed if that APK is uninstalled or updated, and thus | |
| when WebView is updated, all app processes that have loaded it are killed, and | |
| will use the new version if they are restarted. | |
| While this is often frustrating for app developers, it would cause problems if | |
| we didn't kill these processes: | |
| * The process might be in the middle of loading WebView at the time. The APK is | |
| accessed multiple times during loading and if it's been replaced in the meantime | |
| then we might fail to load some part, or load mismatched versions. | |
| * The process is likely to already have open file handles referring to the APK. | |
| This will prevent the APK from actually being deleted until it exits, which may | |
| not be for a long time in the case of some apps (e.g. the launcher or IME). | |
| * Read-only `mmap()`ed pages can't be shared between processes using different | |
| versions of the APK. | |
| * On O+ where the WebView can be multiprocess, any newly launched renderer | |
| process would always be using the currently installed APK to provide its | |
| implementation, causing a mismatch between the browser and renderer code. | |
| ### Changes in N | |
| For security, we want to verify that the implementation we are loading is | |
| legitimate; on earlier versions the platform took care of this for us because | |
| there was only one implementation package (whose name was configured at build | |
| time), and it was always preinstalled, so could never be replaced by a package | |
| with a different signing key. However, on N, we added support for using the | |
| beta, dev, and canary versions of Chrome as WebView implementations as well, and | |
| since these are not preinstalled, the loading mechanism must validate that they | |
| are signed with the expected keys itself. | |
| Asking `PackageManager` for information about the implementation package, | |
| validating it, and then calling `Context.createPackageContext` would have | |
| created a potentially exploitable time-of-check to time-of-use vulnerability: | |
| since `Context.createPackageContext` just takes a package name as a string, the | |
| package may have been replaced after checking it but before loading it. To avoid | |
| this, we switched to the internal `Context.createApplicationContext` API, which | |
| performs the same function but takes an `ApplicationInfo` object describing the | |
| APK to load instead of a package name. We can then pass in the validated info | |
| and if the package has been replaced in the interim, we will fail to load it, | |
| as the replacement APK will have been installed to a new location by | |
| `PackageManager`. | |
| We also explicitly call `ActivityManager` to notify it that we are using the | |
| package beforehand, which ensures that the process will be killed if the package | |
| is removed or replaced, or if the WebView provider is switched. | |
| ## Loading Java code | |
| Loading the Java code is straightforward: the `Context.getClassLoader` method | |
| will give us a `ClassLoader` that refers to the WebView APK, which we can use to | |
| load classes via reflection. The classpath of the classloader should always | |
| contain all relevant things (e.g. APK splits, library APKs) as it's set up by | |
| the same code that is used when the framework loads an APK "normally". | |
| The WebView's classloader is entirely separate to the app's: they are siblings, | |
| with a common parent (the system classloader, which contains the core framework | |
| code). Java classloaders (normally) delegate to their parent before attempting | |
| to load a class themselves, and this means that the app and WebView will always | |
| have the same definition of any framework class. Classes defined in the app APK | |
| are not directly visible to WebView and vice versa. | |
| To avoid having to use reflection throughout the WebView API, we make use of the | |
| fact that the framework is the common parent: the framework defines the WebView | |
| API as a series of interfaces and abstract classes, and the WebView APK defines | |
| classes which implement or subclass them. This still requires one use of | |
| reflection to "bootstrap" the process: `WebViewFactory` must use reflection to | |
| load the class in the WebView APK which implements the framework's | |
| `WebViewFactoryProvider` interface. This interface defines methods that are | |
| used to create (or for singletons, retrieve) instances of all the other required | |
| classes. | |
| * On L - N, the class loaded by reflection is called | |
| `com.android.webview.chromium.WebViewChromiumFactoryProvider`. | |
| * On O+, the class is called | |
| `com.android.webview.chromium.WebViewChromiumFactoryProviderForO` (or `ForP`, | |
| `ForQ`, etc). This was made version-specific to avoid cases where using an | |
| outdated version of WebView on a newer version of Android would crash in | |
| unpredictable ways when new APIs were called - instead, loading will always fail | |
| in a consistent way due to the version-specific provider class being missing. | |
| ### Bugs on O in multiprocess mode | |
| On Android O the WebView zygote, used to spawn renderer processes, doesn't get a | |
| full classpath set up as expected: only the base WebView APK and its splits (if | |
| any) are included in the classpath, and any shared library or static shared | |
| library APKs are omitted. WebView did not normally depend on any library APKs in | |
| O, so this doesn't usually cause an issue, but it's one of the reasons why it's | |
| not possible to use Trichrome on O. | |
| In addition, even though the splits are included in the classpath, the WebView | |
| zygote in O stores the classloader object itself into the cache incorrectly if | |
| there are any splits, which causes a duplicate classloader to be created when | |
| a renderer actually starts up; this results in a crash (as it tries to load the | |
| native library twice, which is forbidden). We work around this in the WebView | |
| code by | |
| [using reflection early in initialization](https://source.chromium.org/chromium/chromium/src/+/main:android_webview/glue/java/src/com/android/webview/chromium/SplitApkWorkaround.java;l=24;drc=4d56a5dd051beb6486d789f50597e66d4267b2e6) | |
| (this class has since been deleted from the chromium repo because the code is no | |
| longer needed). | |
| ## Loading native code with RELRO sharing | |
| Loading the native code in the "usual way" by calling `System.loadLibrary` from | |
| Java would work, as long as the caller was a class in the WebView APK - the JVM | |
| uses the calling class to decide which classloader's native library search path | |
| to use. | |
| However, the WebView's native library has an unusually large `GNU_RELRO` | |
| section. This is the part of the binary which contains data that requires | |
| [relocation](https://en.wikipedia.org/wiki/Relocation_%28computing%29) by the | |
| dynamic linker, but does not need to be writable at runtime. In WebView's case, | |
| this is composed mostly of C++ vtables, as well as other const data structures | |
| that happen to contain pointers to other code and data in the binary, and is | |
| around 2MiB (at time of writing). | |
| We want to avoid each process that uses WebView having a separate copy of the | |
| RELRO section, but since the data depends on the address at which the library | |
| has been loaded, under normal circumstances it can't be shared as different | |
| processes load the library at different addresses. We solve this with a three | |
| step loading process: | |
| ### Step 1 - address space reservation and RELRO generation | |
| The system zygote reserves a chunk of address space at boot time, so that all | |
| processes which eventually load WebView's native library can load it at the same | |
| address. This reservation is made by the framework WebView loading code; the | |
| WebView APK is never loaded into the system zygote for security and logistical | |
| reasons. | |
| * On Android L - P, the size of the reservation to use is stored in a persistent | |
| system property, and if the property isn't set, a default of 100MiB is used. | |
| When WebView is updated on a device, the property is set to double the size of | |
| the `.so` file. On 64-bit devices where there's both a 32-bit and 64-bit `.so` | |
| file, the larger of the two (basically always 64-bit) is used to set the size, | |
| and both the 32-bit and 64-bit system zygotes reserve the same amount of space. | |
| This is not very efficient for the 32-bit zygote, where much less space is | |
| typically needed. 2x is used because it's expected that updated libraries may be | |
| larger than the current version (but almost certainly less than 2x), and it's | |
| hard to determine how much virtual address space will be required for a given | |
| library in the first place without parsing the ELF headers. | |
| * On Android Q, the 32-bit system zygote always reserves 130MiB (the typical | |
| amount used on older versions, more than enough space for the 32-bit library), | |
| and the 64-bit system zygote always reserves 1GiB (as address space is virtually | |
| free on 64-bit). The dynamic sizing code was removed as it was complex and did | |
| not handle static shared library APKs correctly - the amount of space required | |
| in practise has not varied a great deal and hardcoded values suffice. | |
| It's theoretically possible (though very unlikely) for the address space | |
| reservation to fail; if this happens then RELRO sharing is simply skipped. | |
| At boot time, and each time that the WebView is updated, a RELRO creator process | |
| is started for each ABI supported by the device. This process loads the WebView | |
| native code to the reserved address with `android_dlopen_ext` and instructs the | |
| linker to write a copy of the RELRO data out to a file after applying the | |
| relocations. This file is stored in a world-readable system directory. | |
| ### Step 2 - preloading with RELRO | |
| When an app loads WebView, the loading code attempts to load the WebView native | |
| library with `android_dlopen_ext`, passing in the file containing the | |
| preprepared RELRO data. The linker applies the relocations to the library as | |
| usual, but then checks each relocated 4KiB page against the RELRO file, and any | |
| pages which are identical (typically almost all of them, unless the file is | |
| outdated) are replaced with a read-only mapping from the file. This frees up the | |
| memory used by the relocated pages, as the pages mapped from the file can be | |
| shared. If this load fails for any reason then we simply ignore the error and | |
| continue - the RELRO data will not be shared in this process. | |
| This does not make the JVM aware of the library, and does not call `JNI_OnLoad`; | |
| we are only loading it as a generic native library at this point. | |
| * Android L: The native library must be called `libwebviewchromium.so` and must | |
| have been extracted to disk by `PackageManager` at install time to the normal | |
| location where apps' shared libraries are extracted. Loading the library | |
| directly from the APK is not supported: the system linker only gained the | |
| ability to do this in M, and unlike Chrome, WebView can't (at least practically) | |
| use the Chromium linker to work around it. | |
| * Android M - P: The native library filename is specified by a metadata tag in | |
| the APK's manifest, and can be extracted to disk or loaded directly from the | |
| main WebView APK. It will not be found if it's in a split APK or library APK. | |
| This is one of the reasons why Trichrome can't be used on O or P. | |
| * Android L MR1 - P: In addition to the above requirements, a bug in the dynamic | |
| linker introduced in L MR1 means that the native library must not depend on any | |
| other native libraries unless they are already loaded into the process. If more | |
| than one library is loaded at once, the RELRO sections of the libraries will be | |
| corrupted. This is fixed in Q. | |
| * Android Q: The native library filename is specified by a metadata tag in the | |
| APK's manifest, and can be located anywhere in the normal native library load | |
| path for the WebView classloader, including inside split APKs and library APKs, | |
| as the loading code no longer searches for the library file at all and simply | |
| lets it be found by the linker. Any dependencies of the main native library that | |
| are loaded at the same time are also loaded into the reserved address space and | |
| benefit from RELRO sharing. | |
| ### Step 3 - loading with System.loadLibrary | |
| The Java code inside the WebView APK calls `System.loadLibrary` for the main | |
| native library name during initialization. If the library was not already | |
| preloaded in step 2, then the library will actually be loaded by the dynamic | |
| linker now. Once it's loaded (or found), `JNI_OnLoad` will be called to | |
| initialize it, and calls to native methods from Java will work. | |
| Since the default platform library loading mechanism is being used here, there | |
| are no special requirements to enable the library to be found. | |
| Unlike most of the other loading steps, this step is performed by code inside | |
| the WebView APK (in order to call it from the correct classloader context), | |
| which means this step can be changed without changing the framework. | |
| ## Loading assets and resources | |
| The WebView APK contains a number of asset files such as Chromium .pak files, | |
| V8 startup snapshots, ICU data, etc. We also have a bunch of Android resources | |
| such as the strings and layouts for the UI surfaces that WebView exposes (e.g. | |
| the date and color pickers used for HTML5 input elements). | |
| Android exposes assets and resources via `AssetManager`, but rather than use the | |
| `AssetManager` associated with the WebView APK `Context` that was created above, | |
| we instead add the WebView APK to the application's own `AssetManager`. This is | |
| important to ensure that resource references all work correctly: it's possible | |
| for WebView resources (like XML layouts) to end up referencing app resources via | |
| themes, and so the resources have to coexist. | |
| In addition, the WebView APK `Context` is not kept around or made available to | |
| the WebView implementation code, so using the app context is the only easy | |
| option at present. | |
| The `AssetManager` maintains two main pieces of state that are relevant: the | |
| asset path, which is simply a list of all the APKs that it's managing, and a | |
| mapping from package IDs to those APKs (explained further below). | |
| * Android L-P: Only the base WebView APK is added to the application's asset | |
| path. Assets and resources in split APKs or library APKs will not be usable. | |
| This is one of the reasons why Trichrome can't be used on O or P, and also means | |
| that all of WebView's assets and resources must be in the base module when using | |
| bundles. | |
| * Android Q: All the APKs in the WebView's classpath are added to the | |
| application's asset path. | |
| ### Assets | |
| Assets are identified simply by their filename, which can include subdirectory | |
| paths, and they're searched for in the `assets` directory of every APK in the | |
| asset path. Since WebView's assets have been added to the app's asset path, we | |
| can find and load them using the `AssetManager` obtained from the app context. | |
| One complication here is that because the asset path in use contains both the | |
| WebView APK and the app APK, it's possible for asset filenames to collide. It | |
| appears that in this case, WebView's assets take priority, which mean that our | |
| assets work as expected, but the app will get the wrong files and may break. | |
| This is a particular problem for any app that includes Chromium code while also | |
| using the system WebView, since filename collisions are highly likely. | |
| ### Resources | |
| Resources are usually identified by a 32-bit ID, though it is also possible to | |
| look them up by name using | |
| [`Resources.getIdentifier`](https://developer.android.com/reference/android/content/res/Resources#getIdentifier%28java.lang.String,%20java.lang.String,%20java.lang.String%29) | |
| at a cost to performance. The bottom three bytes of the ID identify the specific | |
| resource (one byte type, two byte index), but the top byte of this ID is the | |
| "package ID" assigned by the `AssetManager`. The app's own resources are always | |
| assigned the package ID `0x7f`, and the framework's resources are always | |
| assigned the package ID `0x01`. 0 is invalid, and the values between `0x02` and | |
| `0x7e` are dynamically allocated for library APKs. | |
| When WebView is added to the application's `AssetManager`, it's assigned the | |
| next free package ID, which is typically (but not always) `0x02`. This means | |
| that unlike assets, WebView's resources cannot conflict with the app's; but it | |
| also means that WebView doesn't know the actual ID of its resources at compile | |
| time. | |
| * Android L-M: The WebView APK must have been built using the `--shared-lib` | |
| flag for `aapt`. This flag allows the resource IDs to be assigned at runtime, | |
| instead of assuming that they will begin with `0x7f`. It also makes the | |
| generated R class fields non-final, so that they can be patched at runtime to | |
| have the correct package ID. However, an APK built with this flag will not be | |
| able to use its resources in the normal way if it's launched as an app, instead | |
| of being loaded as a shared library. | |
| * Android N-Q: The WebView APK can be built as on L-M, but can also be built | |
| using the `--app-as-shared-lib` flag instead. This allows the resource IDs to | |
| be assigned at runtime and makes the R class fields non-final as above, but also | |
| makes it possible to load the APK as a regular app with an `0x7f` package ID. | |
| This is what makes Monochrome work, as with the old approach Chrome would not be | |
| able to find its own resources when launched normally. APKs built this way are | |
| not compatible with older OS versions. | |
| WebView is usually loaded as a library and so both modes work, but we do have | |
| some cases where we launch it as a regular app such as the open source license | |
| viewer and the service used to send crash reports. To ensure that these use | |
| cases work correctly on L and M, it's necessary to look up resources by name | |
| using the framework APIs instead of relying on resource IDs. | |
| ### Updating the asset path to work around app incompatibilities | |
| A number of applications do unusual things with contexts, resources, and assets. | |
| Android originally exposed the APIs required for apps to construct their own | |
| instances of `Resources` and to change the `Configuration` being used. These | |
| were not intended to be public and are now deprecated, but many apps still rely | |
| on them. Some apps simply override context methods like `getAssets` and | |
| `getResources`, and do not always maintain the platform invariants. | |
| This sometimes means that the application-level `Context` to which WebView's | |
| APK was added during initialization and the `Context` that a given instance of | |
| WebView is actually using do not share the same `AssetManager` as we expect, | |
| and as a result, WebView may fail to find its resources when using the | |
| instance-specific context. We can't simply always use the app context, because | |
| different contexts may have different themes or configurations that may result | |
| in (correctly) resolving resources differently. | |
| So, to partially work around this, versions of WebView since L MR1 also try to | |
| add the WebView APK to the asset path of the instance-specific context when a | |
| WebView instance is constructed. This is usually a no-op as it's already present | |
| from the app context, but helps some apps work correctly. | |
| Unfortunately this mechanism is not 100% effective and some application usage | |
| patterns relying on deprecated platform APIs still result in a failure to find | |
| WebView resources at runtime, and there's not much we can do about this. |