~/posts/mobile/android-conscrypt-roots-mount-namespaces.md

Android trust: modules, root stores and process views

Trace Android certificate trust across Conscrypt and framework selectors, active APEX modules, process mount namespaces, submounts and disabled roots, using fixed source revisions and Pixel 3a records.

date[31:24]
read[23:16]
9 min
cat[15:8]
Mobile
Contents
  1. 0x00Record both update tracks
  2. 0x01Identify the directory-selection class
  3. 0x02Presence is not activation
  4. 0x03Inspect the target process's mounts
  5. 0x04A plain bind can omit submounts
  6. 0x05Disabled entries and certificate caches
  7. 0x06Close the verification loop
  8. 0x07References

A proxy CA works in the browser, yet an Android application still fails its TLS handshake. Before attributing that difference to pinning, establish where the application process obtains its trust anchors. The platform build, Conscrypt module, certificate-source implementation and mount namespace all matter.

Pixel 3a observations recorded before June 2025 illustrate three separate conditions: a module contains certificates, a process can see them, and an application accepts them. Fixed source revisions help explain those observations; they do not turn a device record into a compatibility guarantee for every phone with the same Android version.

Record both update tracks

Mainline lets selected components update independently of a full OTA. An entry such as "Android 13" omits the Google Play system update (GPSU) and active Conscrypt version. Even the settings page can show three different points in time:

Version fields shown in settings3 rows
Field Recorded value Meaning
Android version 13 Platform release
Android security update 2023-09-01 System patch level
Google Play system update 2025-04-01 Mainline update date

Keep these settings-page observations separate from the Pixel 3a comparisons below; they are not a combined baseline from one device. Collect the following read-only device information as well, subject to the build's access controls:

Device baselinesh
adb shell getprop ro.build.fingerprint
adb shell getprop ro.build.version.sdk
adb shell ls -ld /apex/com.android.conscrypt*
adb shell cat /apex/apex-info-list.xml

Check isActive, rather than assuming that the newest filename identifies the module in use. One recorded active entry contains these fields:

apex-info-list.xml · recorded fieldsxml
<apex-info
    moduleName="com.android.conscrypt"
    modulePath="/data/apex/decompressed/com.android.conscrypt@331411000.decompressed.apex"
    preinstalledModulePath="/system/apex/com.google.android.conscrypt.apex"
    versionCode="331411000"
    isFactory="true"
    isActive="true" />

The isFactory="true" flag matters. A file under /data/apex/decompressed is not, by itself, evidence of a subsequently installed update: a compressed factory APEX also gets extracted there. Interpret the path together with the active flag, preinstalled location and version. The APEX boot and decompression documentation explains this layout.

Identify the directory-selection class

The conventional location is usually /system/etc/security/cacerts; the module location is /apex/com.android.conscrypt/cacerts. Existence alone does not determine which one is read. Two similar selectors also live at different layers:

Two selectors in fixed source revisions2 rows
Class and revision Explicit SDK gate Other conditions for selecting APEX
Conscrypt TrustedCertificateStore, commit a1971d3 SDK available and at least 34 Java property is not the string true; directory exists and is nonempty
Android framework SystemCertificateSource, android-14.0.0_r1 None inside this function Java property is not the string true; directory exists and is nonempty

These are different classes in different source trees, not simply two successive versions of one module function. The framework class ships with the platform. Installing a new Conscrypt APEX does not imply that its framework counterpart has also been replaced.

For a directory that can be listed normally, this illustrative expression captures the Conscrypt condition. The actual source obtains the runtime SDK through getSdkVersion():

Conscrypt selection · logical equivalentjava
boolean useApex(Integer sdk, String property, boolean exists, int entries) {
    return sdk != null
        && sdk >= 34
        && !"true".equals(property)
        && exists
        && entries > 0;
}

Java's System.getProperty("system.certs.enabled") is separate from Android's property service. Setting a same-named value with setprop or resetprop does not establish that this Java property changed. The comparison is also exact: "True" and "true" are different inputs.

Identify the class that actually executes on the device and its build before applying either rule. Compare the pinned Conscrypt source with the Android 14 framework source.

Presence is not activation

The Pixel 3a records distinguish module contents from runtime selection:

Device records: platform and module compared separately4 rows
Environment Conscrypt version Observation
Android 11, RP1A.200720.009 300900703 No APEX cacerts; the intended update was not obtained
Android 12, SP1A.210812.015, before updating 310727000 No APEX cacerts
Same Android 12 installation, GPSU showing 2025-04-01 351412000 APEX cacerts present
Android 11, RQ3A.211001.001, after manually installing that module 351412000 Directory present; the recorded selector still chose the conventional path

A newer layout can therefore appear on an older platform without establishing that Android 11 or 12 generally uses module roots. A separate Android 13 observation depended on the APEX directory, but the earlier module version was not retained and the behavior was not reproduced on an identical build. It does not support a broader version rule.

The relevant package metadata for version 351412000 identifies com.google.android.conscrypt, not the APEX module name com.android.conscrypt:

Package metadata · relevant fieldsxml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.android.conscrypt"
    android:versionCode="351412000">
    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="35" />
    <application android:hasCode="false" />
</manifest>

The package's minSdkVersion="30" does not cancel an SDK 34 gate inside a root-directory selector. Likewise, hasCode="false" does not mean the entire APEX contains no JAR or native library. These are package metadata fields, not an inventory of the APEX payload.

Moving or rolling back modules also depends on signatures, architecture and platform acceptance rules. After a staged installation, reboot and check the active version again. One INSTALL_FAILED_DUPLICATE_PACKAGE error from treating the file as an .apk does not characterize every non-staged installation path.

Inspect the target process's mounts

Only after establishing that the implementation selects the APEX path should the investigation move to file visibility. This diagram is a diagnostic sequence, not a complete TLS call stack:

flowchart TD
  accTitle: From the active implementation to application trust
  accDescr: Identify the certificate source, inspect files in the target namespace, then check disabled entries and application policy.
  A["Certificate-source class actually used"] --> B["Selected system or APEX path"]
  B --> C["Target process mount namespace"]
  C --> D["Visible files and submounts"]
  D --> E["User certificates, disabled entries and caches"]
  E --> F["Application trust configuration and extra checks"]

A root-management module may merge certificate sets, overlay the conventional directory and then bind it onto the APEX path. That still leaves process visibility to verify. Record mount propagation, zygote state and application creation time separately; the current shell does not represent every application's view.

These read-only templates run in a device-side shell. Replace PID with the verified target process ID, and check permissions and the available nsenter implementation first:

Target namespace · inspection templatesh
readlink /proc/PID/ns/mnt
cat /proc/PID/mountinfo
nsenter --mount=/proc/PID/ns/mnt -- \
  ls -l /apex/com.android.conscrypt/cacerts

After changing zygote's view, inspect both newly created applications and processes that already existed. After restarting an application, record whether it came from the expected zygote. A path visible in the shell is not evidence that the application is using those certificates.

A plain bind can omit submounts

In one Android 15 record, individual system certificate files were separate mounts. Their paths included bf64f35b.0, 5acf816d.0 and d41b5e2a.0 under /system/etc/security/cacerts. The filenames identify entries in that mount record; they are not used here to infer certificate subjects.

What the certificate interface actually showed2 rows
State Settings observation Still to check
Normal view Several entries visible on the system tab The path read by the application
Abnormal view No entries displayed on the system tab File contents, submounts, read access and caches

An empty interface establishes a display result, not a device-wide certificate count of zero. A plain --bind does not recursively copy the source's submounts. --rbind handles the mount subtree, with unbindable subtrees pruned. The Linux shared-subtree documentation defines that distinction.

The corresponding operation has this form on a test device whose source tree and propagation relationships have been checked. Tool paths and supported options depend on the installed implementation:

Recursive bind · device-specific templatesh
nsenter --mount=/proc/PID/ns/mnt -- \
  mount --rbind /system/etc/security/cacerts \
  /apex/com.android.conscrypt/cacerts

This addresses a particular mount-tree problem, not every interception failure. Save the target namespace's mount baseline, compare submounts and actual files afterward, and restore the recorded hierarchy when reverting instead of stacking more binds. SELinux labels, certificate encoding and an application's independent trust store remain separate checks.

Disabled entries and certificate caches

A system CA can remain in a read-only directory while the user disables it. In the pinned Conscrypt implementation, deleteCertificateEntry() handles a system entry by writing its certificate into the deleted-entry directory. getTrustAnchor() checks candidate system anchors against that state. User-added entries take a different deletion branch.

The framework's SystemCertificateSource checks a same-named marker in the current Android user's cacerts-removed directory. UserCertificateSource reads that user's cacerts-added directory. Common locations for user 0 are shown below; resolve other users from their actual user IDs:

Per-user certificate state · user 0text
/data/misc/user/0/cacerts-added
/data/misc/user/0/cacerts-removed

Do not apply those paths to every standalone Conscrypt instance. The pinned commit defaults to $ANDROID_DATA/misc/keychain and exposes setDefaultUserDirectory(). The caller's configuration determines the final location.

There is also cached state. Android 14's DirectoryCertificateSource caches enumeration results. Its handleTrustStorageUpdate() clears that set without reselecting the directory supplied to the constructor. Distinguish invalidating the certificate set from reconstructing the source object when testing a path or property change.

Close the verification loop

An offline check used javac 13-ea. It extracted shouldUseApex() from the fixed commit and replaced only the SDK-query entry point. The framework function retained its logic with its paths redirected to local fixtures. Cases covered five SDK inputs, four property values and three directory states; the readable nonempty directory held a marker file, not a simulated valid certificate.

Offline selector checkslog
PASS Conscrypt source method: 60 cases
PASS framework source method with redirected fixture paths: 12 cases

These results validate selector branches. They are not new Pixel runs, mount experiments or TLS handshakes. Device validation still needs to reach the application:

From version evidence to connection results4 steps
  1. 1

    Freeze the baseline

    Record the build, SDK, GPSU date, active APEX, actual class origin and process start time.

  2. 2

    Check the file view

    Compare certificate fingerprints, directories and submounts in the target namespace. Record permissions and the user ID.

  3. 3

    Resolve trust state

    Separate system roots, user additions, disabled entries and cached objects. Record state before and after restarting.

  4. 4

    Repeat the same request

    Hold the hostname, certificate chain and application input constant. Compare results, then restore the baseline and check the original behavior.

Whether a modern application trusts user CAs depends on its target API and Network Security Configuration. A custom TrustManager, native networking stack or pinning can add further conditions. When the application is yours to modify, explicitly configuring debug-build trust anchors is more controlled than depending on device-wide mount changes. File visibility is a necessary diagnostic check, not the final trust decision.

References

NORMAL~/posts/mobile/android-conscrypt-roots-mount-namespaces.md§--
0%en