Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/saf/device/evidence/qcril_full_provisioning.md
Fimeg bde961c6f2 saf: one spine — device, state, and work under the index
PAF becomes saf/device (history kept), STATE.md dissolves
into saf/state.md with the dated era archived, the substrate
SAF moves up from souveraine, and every agreement points at
saf/INDEX.md and nowhere else. one map, nothing to remember
2026-08-18 09:47:30 -04:00

680 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# QCRIL Full Provisioning — Gap Analysis & Design
**Purpose:** Reconstruct what QCRIL does to provision the modem every boot, identify every gap
vs. our current Python tools, and design a full replacement provisioner. This feeds a real
hardware bring-up; every structural claim cites a symbol, string, or file.
---
## A. What Our Tools Do Today
Two scripts, manually invoked in sequence.
### pdc_load.py (tools/pdc_load.py)
A chunked PDC LoadConfig implementation via libqmi GI bindings (1024-byte chunks). Reads an MBN
file, computes its SHA-1, and uploads it to the modem via `QMI_PDC_LOAD_CONFIG` with a rolling
token. Uses `EXPECT_INDICATIONS` flag so the modem can ack each chunk. On `remaining_size == 0`
the upload is complete. Accepts `platform` or `software` config type on the CLI.
**QMI sequence:**
1. Open QMI device (QRTR node)
2. Allocate PDC client
3. Connect `load-config` indication handler
4. Loop: `QMI_PDC_LOAD_CONFIG_REQ` (chunk) → indication → next chunk
5. Terminate on `remaining_size == 0`
No version checking. No selection. No activation. No carrier selection. No EFS interaction.
No SIM awareness. No timing logic.
### pdc_activate.py (tools/pdc_activate.py)
Implements the `SetSelectedConfig``ActivateConfig` two-step per the libqmi qmicli source
for `run_activate_config`. Takes a hex config ID on the CLI.
**QMI sequence:**
1. Open QMI device
2. Allocate PDC client
3. `QMI_PDC_SET_SELECTED_CONFIG_REQ` (type + ID + token)
4. On `set-selected-config` indication → `QMI_PDC_ACTIVATE_CONFIG_REQ` (type + token)
5. On `activate-config` indication → done (modem restart expected)
No version check (always activates). No load-first. No sequencing of HW before SW.
No handling of the restart-then-load ordering. No awareness of what was already active.
### modem_health.sh (tools/modem_health.sh)
Read-only snapshot: remoteproc state, QMI up/down, PDC list for platform and software configs
with active flags, DMS get-capabilities (the RF-arm signal), NAS home network, operating mode,
ModemManager status. Non-mutating by contract.
**What this tells us:** the `DMS Networks: ''` == RF not armed. `PDC Total: 0` == modem reset
since last provisioning.
---
## B. What QCRIL Actually Does
Evidence sources: `libril-qc-hal-qmi.so` (34 MB, arm64 ELF, class `main`); embedded source
paths `vendor/qcom/sm7250/proprietary/qcril-hal/modules/mbn/src/`; `qcril.db` (sqlite3);
`mbn_hw.txt`, `mbn_sw.txt`, `mcfg.version`; `qcrild.rc`, `init.sdm845.rc`.
### B.1 Timing — When QCRIL Runs vs Modem Boot
From `qcrild.rc`:
```
service vendor.qcrild /vendor/bin/hw/qcrild
class main
user radio
```
`class main` services start at the `main` class trigger, which fires after `post-fs-data`
completes. From `init.sdm845.rc`, the modem subsystem is NOT explicitly gated here — there is
no `wait_for_prop` on a modem-ready property before qcrild starts. `rmt_storage` (EFS daemon)
is also `class core` and starts earlier.
The modem firmware itself (MPSS via remoteproc `q6v5-mss`) loads at ~12.8 s after Linux boots,
independent of Android init stages. QCRIL starts asynchronously as part of `class main` (roughly
after `post-fs-data`, typically 1030 s into boot, well before userspace is fully up).
**The decisive timing insight** (from PROGRESS.md 2026-06-20 entry): the modem applies MCFG at
its own firmware RF-init window, which occurs during/after MPSS comes up (~12 s). QCRIL must
deliver configs within this window. QCRIL achieves this because `qcrild` (class main) starts
early and its MBN update sequence begins as soon as the DMS client becomes ready — triggered by
`DmsModule::handleDmsEndpointStatusIndMessage``qcril_qmi_start_mbn_update()`.
**The trigger:** strings `"DMS client is ready. start MBN update"` and
`"Module is ready,Start Mbn update"` both come from the `DmsModule` source
(`vendor/qcom/sm7250/proprietary/qcril-hal/modules/dms/src/DmsModule.cpp`). When DMS QMI
service becomes available on QRTR, `DmsModule::handleDmsEndpointStatusIndMessage` fires and
calls `qcril_qmi_start_mbn_update()` (nm: `_Z26qcril_qmi_start_mbn_updatev`). This is the
entry point into the full HW+SW MBN update state machine.
**Critical observation:** QCRIL does NOT wait for the modem to be "online" or for an RF-init
complete signal before loading configs. It loads as soon as DMS is up (the modem is in some
early QMI-ready state but not yet RF-armed). The modem then applies the loaded MCFG at its
own RF-init pass, which happens after configs are in place.
The string `"is_ssr_or_bootup %d"` (`qcril_mbn_sw_update.cpp`) shows that the SW update path
distinguishes cold boot from SSR. On warm SSR (subsystem restart), QCRIL only queries — does
NOT reload (`qcril_qmi_pdc_get_active_config_info`, `qcril_qmi_pdc_get_selected_mbn_config`).
On cold bootup, it runs the full load sequence. This matches the Android forensics finding:
QCRIL skips reload on warm SSR (configs survived) but loads fresh on cold boot.
### B.2 Config Discovery — Where QCRIL Finds MBN Files
Two source directories (strings in libril-qc-hal-qmi.so):
- Primary: `/data/vendor/modem_config/` (writable, symlinked or copied from vendor)
- Vendor source: `/vendor/rfs/msm/mpss/readonly/vendor/mbn/` (read-only vendor partition)
On Android, QCRIL reads `mcfg.version` from `/vendor/rfs/msm/mpss/readonly/vendor/mbn/mcfg.version`
(path literal in strings). It stores local DB metadata at
`/data/vendor/modem_config/ver_info.txt` and in `qcril.db` tables.
The hardware config tree is indexed by `mbn_hw.txt`; the software config tree by `mbn_sw.txt`.
QCRIL enumerates these lists to populate its internal DB (`qcril_mbn_hw_load_to_db`,
`qcril_mbn_sw_load_to_db`) with version metadata parsed from each MBN file's header.
Error strings `"QCRIL_ERROR:IO: No hw mbn config directory"` and
`"QCRIL_ERROR:IO: No sw mbn config directory"` confirm it expects both
`/data/vendor/modem_config/mcfg_hw/` and `/data/vendor/modem_config/mcfg_sw/` to exist and
be populated. The MBN path is also stored in `persist.vendor.radio.mbn_path` (property string
in libril-qc-hal-qmi.so).
**On pmOS:** our MBN files live at `/mnt/vendor/persist/rfs/readonly/vendor/mbn/` (bind-mounted
from the persist partition, analogous to tqftpserv's serve path). We have both `mcfg_hw/` and
`mcfg_sw/` trees. The provisioner must be told where to find them.
### B.3 Config Selection — How QCRIL Picks the Right MBN
#### Hardware MBN Selection
Symbol `qcril_mbn_hw_get_hw_name_to_look_for` (T, exported): looks up the hardware platform
name to match against. Symbol `qcril_mbn_hw_get_hw_config_from_db` looks up the best matching
HW config from the internal DB by that name.
The HW name comes from the MBN metadata header embedded in each `mcfg_hw.mbn` file. Symbol
`qcril_mbn_meta_retrieve_hw_name` reads the `HW_NAME` field from the MBN binary. The DB table
`qcril_hw_mbn_file_type_table` stores `(FILE, HW_NAME, SHORT_NAME, CONFIG_ID, version fields)`.
The selection is a name-match: QCRIL identifies the running SoC/platform (from a system property
or DMS device ID) and picks the HW MBN whose `HW_NAME` matches. For blueline/SDM845 + LA + SS
(single-SIM), the match is `mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`
(verified: only SDM845/LA/SS and SDM845/LA/7+7_mode/SR_DSDS are in `mbn_hw.txt`; blueline is
single-SIM so SS wins). The `HW_NAME` field in that MBN file must contain the identifier QCRIL
matches against.
**Note:** The `HW_NAME` used for matching is extracted from the MBN binary header by
`qcril_mbn_meta_retrieve_hw_name` — we don't have a text listing of what that string IS. For
blueline we know the correct MBN from Android forensics: `mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`.
Our provisioner can hardcode this for SDM845/LA/SS rather than implement the full name extraction.
#### Software MBN Selection (Carrier)
This is the complex path. QCRIL uses a multi-level lookup in priority order:
**Level 1 — ICCID long-IIN (9-digit):** `qcril_mbn_db_retrieve_sw_mbn_file_for_long_iccid`
queries `qcril_sw_mbn_iin_table` with the first 9 digits of the SIM's ICCID (`MCFG_LONG_IIN`
field). If matched, uses that SW MBN. The ICCID is stored in `qcril_mbn_sw_iccid` (B, bss) and
the current ICCID is read from `/data/vendor/radio/iccid` (path string in libril-qc-hal-qmi.so)
or via UIM QMI `get_iccid` (`com.qualcomm.qti.qcril.uim.get_iccid_sync_request`).
**Level 2 — ICCID short-IIN (6-digit):** `qcril_mbn_db_retrieve_sw_mbn_file_for_iccid` queries
`qcril_sw_mbn_iin_table` with the first 6 digits (`MCFG_IIN`).
**Level 3 — MCC/MNC:** `qcril_mbn_db_query_sw_mbn_file_with_mcc_mnc` queries
`qcril_sw_mbn_mcc_mnc_table` with the SIM's MCC+MNC. MCC/MNC is retrieved via
`com.qualcomm.qti.qcril.legacy.event.INTERNAL_UIM_GET_MCC_MNC` and/or the UIM IMSI path.
The DB tables (`qcril_sw_mbn_iin_table`, `qcril_sw_mbn_mcc_mnc_table`) are empty in the
`qcril.db` we have — QCRIL populates them at runtime by parsing the MBN files and inserting
rows. The `qcril.db` version in `qcril-config/` is a prebuilt baseline; QCRIL rebuilds the MBN
tables on first boot or when `mcfg.version` changes.
The `qcril_manual_prov_table` contains 4 ICCID entries with `USER_PREF=1` — these are
user-overridden manual provisioning entries that skip the automatic lookup. Our Fido SIM is
unlikely to match these.
**Exception tables:** `qcril_mbn_iccid_exception_table` and `qcril_mbn_imsi_exception_table`
are both empty in this build — no exceptions defined.
**For Rogers/Fido (MCC 302):** Fido is an MVNO on the Rogers network. The MBN tree contains
`mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn` (line 25 in `mbn_sw.txt`). Rogers MCC
is 302. Whether QCRIL matches Fido (sub-MVNO) to the Rogers MBN depends on whether Rogers's
ICCID/IIN prefix or MCC/MNC 302-480 (Rogers) vs 302-370 (Fido) maps to that file in the
runtime-built DB. QCRIL's logic also has a `persist.vendor.radio.sw_mbn_openmkt` (open-market)
flag that may affect fallback behavior — evidence: property string in libril-qc-hal-qmi.so.
**IMEI awareness:** QCRIL does NOT use the IMEI for MBN selection. IMEI is read via
`RilRequestGetDeviceIdentityMessage` / `DmsModule::handleDeviceIdentiyRequestMessage` for
device identity reporting, not for PDC config selection. The `qcril_mbn_cur_instance_id` (B)
is the slot/instance ID (0/1), not the IMEI.
**Subscription tracking:** QCRIL caches the current subscription's ICCID
(`qcril_mbn_sw_iccid`), MCC (`qcril_mbn_cur_instance_mcc`), MNC (`qcril_mbn_cur_instance_mnc`),
and sub_id (`qcril_mbn_cur_sub_id`). When any of these change (SIM swap), `qcril_mbn_sw_is_sim_info_different_from_cache`
detects it and `qcril_mbn_sw_if_restart_needed` decides whether to re-run the SW provisioning
sequence. String: `"restart needed due to mcc/mnc/iccid/sub_id change"`.
### B.4 Version/Diff/Skip Logic
Symbols `qcril_mbn_db_is_sw_version_updated` (T, exported) and the string
`"file_name: %s, config_name: %s, version: 0x%08x, is_matched: %d"` describe the core
version-check logic:
QCRIL parses each candidate MBN's version from its header (`mcfg_get_oem_version`,
`mcfg_get_qc_version` — both exported T symbols) and compares them to what the modem currently
holds. The modem's active config version is retrieved via `qcril_qmi_pdc_get_active_config_info`
(which calls `QMI_PDC_GET_CONFIG_INFO` with the active config ID).
Version is stored as a 32-bit split field: `MCFG_VERSION_FAMILY`, `MCFG_VERSION_OEM`,
`MCFG_VERSION_CARRIER`, `MCFG_VERSION_MINOR` (function `qcril_mbn_db_split_version`, T exported).
If the modem's active config version matches the candidate MBN's version (`is_matched == 1`),
QCRIL skips the reload. This is the "already provisioned" fast path tracked via Android
property `persist.vendor.radio.hw_mbn_loaded` / `persist.vendor.radio.sw_mbn_loaded` and
`persist.vendor.radio.cnv.ver_info`. The string `"prev_ver_info: %s, cur_ver_info: %s"`
shows QCRIL compares a cached version string (from persist prop) to the current MBN version
before deciding to reload.
The event `QMI_RIL_PDC_PARSE_DIFF_RESULT` (`qcril_evt_id_QMI_RIL_PDC_PARSE_DIFF_RESULT`) and
function `parse_mbn_diff_result` indicate that QCRIL can also interpret a diff-result indication
from the modem to decide whether the new config differs enough from the active one to warrant
a restart. String: `"mbn differences length ="`.
**Summary of skip conditions:**
1. `persist.vendor.radio.hw_mbn_loaded` / `sw_mbn_loaded` is set AND version matches modem active config → skip reload (fast path).
2. On warm SSR: query get_active_config_info; if active matches → skip reload.
3. On cold boot with no active config (count=0): always load.
**For pmOS:** Since configs are volatile (proven: 0/0 after every cold boot, PROGRESS.md
2026-06-20), version check is moot — there is never an existing active config on our cold boot.
We always need to load. The skip logic matters only when we build a persistent-prop mechanism
or if we add SIM-change re-provisioning.
### B.5 The Full Load → Select → Activate Sequence
Reconstructed from the symbol table event chain:
#### Phase 1: HW MBN (platform config) — `REQUEST_MBN_HW_*` events
1. `REQUEST_MBN_HW_INIT``qcril_mbn_hw_update_init_hdlr`: Initialize HW update state machine.
2. `REQUEST_MBN_HW_GET_SELECTED_CONFIG``qcril_mbn_hw_query_selected_config_hndlr`:
Call `QMI_PDC_GET_SELECTED_CONFIG` for type=PLATFORM. If active config matches candidate,
skip to SW phase.
3. `REQUEST_MBN_HW_LOAD_CONFIG``qcril_mbn_hw_load_config_hndlr`:
Chunked `QMI_PDC_LOAD_CONFIG` for the selected HW MBN file.
4. `REQUEST_MBN_HW_SELECT_CONFIG``qcril_mbn_hw_select_config_hndlr`:
`QMI_PDC_SET_SELECTED_CONFIG` for type=PLATFORM with the just-loaded config ID.
5. `REQUEST_MBN_HW_ACTIVATE_CONFIG``qcril_mbn_hw_activate_config_hndlr`:
`QMI_PDC_ACTIVATE_CONFIG` for type=PLATFORM. Modem restarts MPSS to apply MCFG.
Interleaved: `REQUEST_MBN_HW_DELETE_CONFIG` and `REQUEST_MBN_HW_DEACTIVATE_CONFIG` are also
in the symbol table; QCRIL cleans up old/stale configs. `qcril_mbn_cleanup_inactive_configs`
and `qcril_mbn_pdc_delete_all_sw_configs` delete inactive entries from modem RAM before loading
new ones (to avoid hitting the modem's config count limit).
#### Phase 2: SW MBN (carrier config) — `REQUEST_MBN_SW_*` events
After HW phase completes (modem has restarted), SW phase runs:
1. `REQUEST_MBN_SW_INIT``qcril_mbn_sw_update_init_hdlr`: Start SW state machine; read ICCID
and MCC/MNC from SIM (requires UIM to be up and SIM app selected).
2. `VERIFY_MBN_SW_INIT` / `REQUEST_VERIFY_MBN_SW_INIT`: Check whether modem supports the MBN
update feature at all (`qcril_qmi_imss_query_modem_supported_features`). String:
`"Modem feature not supported. Continue with sw mbn update"` — if unsupported, SW update
continues anyway (the feature check gates VoLTE-specific paths, not the basic PDC load).
3. `REQUEST_MBN_SW_GET_SELECTED_CONFIG``qcril_mbn_sw_query_selected_config_hndlr`:
Query `QMI_PDC_GET_SELECTED_CONFIG` for type=SOFTWARE. Version-check against candidate.
4. `REQUEST_MBN_SW_COUNT_PENDING_CONFIGS``qcril_mbn_sw_count_pending_configs_hndlr`:
Count pending (loaded but not yet selected) SW configs on the modem. String:
`"has pending configuration"`. If a pending config already matches the desired SW MBN, QCRIL
may skip the load step and go directly to select.
5. `REQUEST_MBN_SW_LOAD_CONFIG``qcril_mbn_sw_load_config_hndlr`:
Chunked `QMI_PDC_LOAD_CONFIG` for the selected SW MBN file (type=SOFTWARE).
6. `REQUEST_MBN_SW_SELECT_CONFIG``qcril_mbn_sw_select_config_hndlr`:
`QMI_PDC_SET_SELECTED_CONFIG` for type=SOFTWARE.
7. `REQUEST_MBN_SW_ACTIVATE_CONFIG``qcril_mbn_sw_activate_config_hndlr`:
`QMI_PDC_ACTIVATE_CONFIG` for type=SOFTWARE. Second modem restart.
8. `REQUEST_MBN_SW_CLEANUP_CONFIG``qcril_mbn_sw_cleanup_config_hndlr`: Delete stale configs.
#### Phase 3: PDC Refresh Indication
`qcril_qmi_pdc_refresh_ind_hdlr` (`_Z30qcril_qmi_pdc_refresh_ind_hdlrPvj`) and
`PDCRefreshIndication` / `"handlePDCRefreshInd(): "` — the modem can emit a PDC refresh
indication after activation. QCRIL handles this to re-query config state. The `DataModule` also
handles `PDCRefreshIndication` (`_ZN7rildata10DataModule26handlePDCRefreshIndicationENSt3__110shared_ptrI7MessageEE`).
This is the modem telling userspace "I've applied the new configs." QCRIL listens for it but
does NOT depend on it to proceed (it's a notification, not a gate).
#### The enable/disable modem update mechanism
Symbols `qcril_qmi_pdc_enable_modem_update`, `qcril_qmi_pdc_disable_modem_update`,
`pdc_enable_auto_selection` indicate that before loading configs, QCRIL may call
`QMI_PDC_SET_FEATURE_VERSION` or a similar PDC command to enable/lock auto-selection on the
modem side. `qcril_qmi_pdc_is_modem_mbn_updated` checks if the modem already has the desired
config loaded (query path, not load path). **The exact QMI message ID for enable/disable is not
visible from strings alone** — this is an under-documented PDC message not exposed via libqmi
public API. Evidence is ambiguous; mark as uncertain.
### B.6 EFS Interaction
Minimal and indirect. QCRIL itself does NOT write to EFS for config provisioning. The evidence:
- `current MBNs in modem EFS:` (string in libril-qc-hal-qmi.so) — QCRIL can enumerate what's
in modem EFS via a PDC query command (list configs), but this is a read.
- Android forensics (PROGRESS.md 2026-06-19): `rmt_storage` serves EFS read-write; the modem
writes ~2 MB to `modem_fs2` AFTER going online. This EFS write happens INSIDE the modem
firmware after RF-init completes — not driven by QCRIL.
- The `EFS lead at PAF/modem.md L38` is marked DEAD: rmtfs writable + Android-primed EFS still
gave DeviceNotReady. EFS content is not what's missing.
**Conclusion:** QCRIL does not write EFS for PDC provisioning. It loads configs into modem RAM
via QMI PDC, the modem applies them at RF-init and then writes its own EFS state. Our provisioner
does not need to write EFS.
### B.7 The OTA Update Path (not needed for us)
`qcril_mbn_kick_ota_update_in_dedicated_thread` handles over-the-air config updates (carrier
pushing new MBNs). This runs in a dedicated thread, separate from boot provisioning. Not
relevant to our bring-up.
### B.8 Properties Used by QCRIL
| Property | Purpose |
|---|---|
| `persist.vendor.radio.hw_mbn_loaded` | Cached: HW MBN was loaded (skip on next boot if version matches) |
| `persist.vendor.radio.sw_mbn_loaded` | Cached: SW MBN was loaded |
| `persist.vendor.radio.mbn%d` | Per-slot carrier index (e.g., `mbn0=72`) |
| `persist.vendor.radio.mbn_path` | Path to the active MBN directory |
| `persist.vendor.radio.cnv.ver_info` | Cached version info string (prev vs current comparison) |
| `persist.vendor.radio.ver_info` | Version info |
| `persist.vendor.radio.sw_mbn_update` | SW update enabled/disabled flag |
| `persist.vendor.radio.hw_mbn_update` | HW update enabled/disabled flag |
| `persist.vendor.radio.sw_mbn_openmkt` | Open-market SIM handling flag |
| `persist.vendor.radio.long_iin_mbn` | Use long-IIN (9-digit) matching for SW MBN |
These are Android system properties not directly available on pmOS. Our provisioner should
implement equivalent state tracking in a config file (e.g., `/var/lib/qcril-prov/state.json`).
---
## C. The Gap — Itemized
Each item marked: **(RF-arm)** = required to arm RF at all, **(carrier)** = carrier-correctness,
**(nice)** = quality improvement.
| # | Gap | Severity |
|---|---|---|
| 1 | **Timing: provisioner must run before RF-init window closes** — our scripts run manually; QCRIL runs automatically on DMS-service-ready event, before `sys.boot_completed`. A systemd service that fires before ModemManager and monitors DMS readiness is absent. | **(RF-arm)** |
| 2 | **HW MBN must be loaded before SW MBN** — our scripts don't enforce ordering. QCRIL runs the full HW phase (load+select+activate → modem restart) before starting SW phase. Interleaving or reversing causes modem inconsistency. | **(RF-arm)** |
| 3 | **Post-activate modem restart handling** — on `QMI_PDC_ACTIVATE_CONFIG` the modem subsystem restarts. QCRIL re-waits for DMS to become ready after each restart before proceeding to the next phase (SW after HW). Our script ignores the restart and the QMI transport drop. | **(RF-arm)** |
| 4 | **SW MBN carrier selection by ICCID/MCC-MNC** — we hardcode the Rogers MBN path. QCRIL reads ICCID from `/data/vendor/radio/iccid` and queries UIM MCC/MNC, then does a DB lookup. Without this, a SIM swap would need a manual provisioner edit. | **(carrier)** |
| 5 | **Stale config cleanup before loading** — QCRIL calls `qcril_mbn_cleanup_inactive_configs` / `qcril_mbn_pdc_delete_all_sw_configs` to delete old configs before loading new ones. Without cleanup, the modem accumulates stale RAM-configs and may hit internal limits or pick the wrong active config. | **(RF-arm / carrier)** |
| 6 | **SW pending config check** — QCRIL calls `QMI_PDC_QUERY_PENDING_CONFIG` (event `REQUEST_MBN_SW_QUERY_PENDING_CONFIG`) before loading to see if the desired config is already pending. Missing this means redundant loads, not a breakage, but it is part of the correct sequence. | **(nice)** |
| 7 | **Version skip logic** — QCRIL skips reload when modem already holds matching config version. Without this, we reload every boot unconditionally (harmless functionally but slow and causes an extra modem restart). | **(nice)** |
| 8 | **PDC modem-update enable/disable** — QCRIL may call a PDC feature-version command around the load. The exact behavior is unclear from strings alone (ambiguous evidence). May be needed to unlock PDC loading on certain modem states. **UNCERTAIN — needs disassembly to confirm.** | **(RF-arm, uncertain)** |
| 9 | **SIM-change re-provisioning** — QCRIL monitors UIM refresh indications and re-runs SW provisioning on SIM swap. Not needed for single-boot bring-up but needed for production use. | **(nice)** |
| 10 | **HW name extraction from MBN header** — QCRIL reads the `HW_NAME` field from each `mcfg_hw.mbn` binary to populate the DB, then matches it against the running platform. We can hardcode SDM845/LA/SS for blueline, deferring this. | **(nice for generality)** |
---
## D. Design — Full Provisioner
### D.0 Guiding Constraints
- Must complete (both HW and SW activated) before the modem's RF-init window closes. From
PROGRESS.md: PDC LoadConfig times out if attempted before ~45 s uptime; succeeds by ~226 s.
The RF-init window is within the modem's post-MPSS-up initialization, which is between ~12 s
and some unknown deadline (likely within 60 s of MPSS-up). **The critical open question:**
does the modem wait indefinitely for a config or does it time out and RF-init with whatever
is loaded? This must be determined empirically (see Next Steps).
- Run as a systemd service, ordered before ModemManager, after QRTR/QMI is up.
- Read-only EFS — no EFS writes needed from our side.
- Python (libqmi GI) to reuse pdc_load.py/pdc_activate.py primitives.
### D.1 Service Unit
File: `/etc/systemd/system/qcril-prov.service`
```ini
[Unit]
Description=QMI PDC MBN provisioner (QCRIL equivalent)
After=qrtr.service rmtfs.service
Before=ModemManager.service
Wants=qrtr.service rmtfs.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/qcril-prov.py
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
```
### D.2 Provisioner Script Design (`qcril-prov.py`)
#### Stage 0: Wait for DMS
Poll `qmicli -d qrtr://0 --dms-get-ids` until it returns IMEI without error. Timeout 120 s.
This replicates QCRIL's `DmsModule::handleDmsEndpointStatusIndMessage` trigger. Retry interval:
2 s. Add a jitter-free deadline check (not a sleep loop — use subprocess with timeout).
EVIDENCE: string `"DMS client is ready. start MBN update"` from DmsModule.
#### Stage 1: Read SIM Identity (for SW MBN selection)
Read ICCID and MCC/MNC before touching PDC, as QCRIL does.
- Option A (simple): read `/data/vendor/radio/iccid` (path string in libril-qc-hal-qmi.so).
On pmOS this file may not be populated by anything. Fallback to QMI.
- Option B (robust): `qmicli --uim-get-card-status` to get ICCID, then
`qmicli --nas-get-home-network` or `--uim-get-imsi` to get MCC/MNC.
For Rogers/Fido: MCC=302, MNC=480 (Rogers) or 302-370 (Fido).
The mbn_sw.txt has `mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn` — this is the file
to use. Fido uses Rogers infrastructure (same MCC prefix 302); whether the DB lookup maps Fido's
IIN/MNC to the Rogers MBN depends on what was in the runtime-built DB on Android. Empirically,
the modem ran Fido LTE on Android with the Rogers MBN active, so Rogers/CA is the correct SW
MBN for both carriers on this device. Hardcode Rogers for now; add MCC/MNC dispatch later.
#### Stage 2: Delete All Existing Configs
Replicate `qcril_mbn_cleanup_inactive_configs` / `qcril_mbn_pdc_delete_all_sw_configs`.
Use `QMI_PDC_LIST_CONFIGS` (both types) → for each config ID returned, call
`QMI_PDC_DELETE_CONFIG`. This clears the modem's RAM before we load fresh.
EVIDENCE: event `QMI_RIL_PDC_DELETE_ALL` (`qcril_evt_id_QMI_RIL_PDC_DELETE_ALL`) and
`qcril_mbn_pdc_delete_all_sw_configs`.
Implementation via libqmi GI: `Qmi.MessagePdcDeleteConfigInput.new()`, set type and config_id,
call `client.delete_config()`. Loop over both PLATFORM and SOFTWARE types.
**Why needed:** avoids stale config accumulation and ensures we control what's active. On cold
boot the list is 0/0 (PROGRESS.md 2026-06-20), so this is a no-op on cold boot but defensive
for warm SSR.
#### Stage 3: Load + Select + Activate HW MBN
MBN file: `/mnt/vendor/persist/rfs/readonly/vendor/mbn/mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`
(56 KB, confirmed in PAF/modem.md)
1. **Load:** use `pdc_load.py` logic (already working) with type=PLATFORM.
2. **Get config ID:** after load completes, call `QMI_PDC_LIST_CONFIGS` type=PLATFORM to find
the ID of the just-loaded config. (The LoadConfig indication does not return the ID;
ListConfigs does.)
3. **Select:** `QMI_PDC_SET_SELECTED_CONFIG` type=PLATFORM, id=<returned ID>.
4. **Activate:** `QMI_PDC_ACTIVATE_CONFIG` type=PLATFORM. Modem MPSS restarts.
EVIDENCE: full symbol chain `qcril_mbn_hw_load_config_hndlr``qcril_mbn_hw_select_config_hndlr`
`qcril_mbn_hw_activate_config_hndlr` in libril-qc-hal-qmi.so nm output.
#### Stage 4: Wait for Modem to Come Back After HW Restart
After HW activation, the modem MPSS restarts. QCRIL detects this via QMI transport drop +
re-registration of DMS service. Poll same as Stage 0. Timeout 60 s.
Without this wait, the SW load in Stage 5 will fail (PDC client not available / modem not yet
QMI-ready).
EVIDENCE: pdc_activate.py comment: "config activation is expected to reboot the device"; modem
restart events `QCRIL_EVT_QMI_RIL_MODEM_RESTART_*`; PROGRESS.md 2026-06-19 "qmicli
--pdc-activate-config=platform triggered modem restart that cleared the platform config."
#### Stage 5: Load + Select + Activate SW MBN
MBN file (Rogers/CA): `/mnt/vendor/persist/rfs/readonly/vendor/mbn/mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn`
(MCC=302 carrier confirmed active on Android in PROGRESS.md 2026-06-19)
Same sequence as Stage 3 but type=SOFTWARE.
1. Load (chunked, type=SOFTWARE)
2. List configs → get ID
3. SetSelectedConfig (type=SOFTWARE)
4. ActivateConfig (type=SOFTWARE) → second modem restart
EVIDENCE: `qcril_mbn_sw_load_config_hndlr``qcril_mbn_sw_select_config_hndlr`
`qcril_mbn_sw_activate_config_hndlr` in nm output.
#### Stage 6: Wait and Verify
After SW activation, wait for DMS to come back (Stage 0 again). Then run the modem_health.sh
check logic inline: call `QMI_DMS_GET_CAPABILITIES` and verify `Networks` is non-empty.
If `Networks` is non-empty, provisioning succeeded. Set a state file
(`/var/lib/qcril-prov/last_provisioned`) and exit 0 so systemd marks the service complete.
If `Networks` is still empty: log a diagnostic (PDC list state, DMS caps) and exit non-zero.
Systemd will not start ModemManager (it's ordered After this service).
#### Stage 7: Hand Off to ModemManager
With both configs active and RF armed (Networks populated), ModemManager's
`Set Operating Mode=online` should succeed. ModemManager starts as normal.
### D.3 SW MBN Selection Logic (future)
When we want auto-carrier selection (beyond hardcoded Rogers):
```python
# Selection priority order (matches QCRIL):
# 1. ICCID long-IIN (9 digits) → qcril_sw_mbn_iin_table.MCFG_LONG_IIN
# 2. ICCID short-IIN (6 digits) → qcril_sw_mbn_iin_table.MCFG_IIN
# 3. MCC+MNC → qcril_sw_mbn_mcc_mnc_table
# 4. Wildcard → mcfg_sw/generic/common/WildCard/Wildcard/mcfg_sw.mbn
def select_sw_mbn(iccid, mcc, mnc, db_path):
import sqlite3
db = sqlite3.connect(db_path)
# Populate DB from MBN tree if empty (QCRIL does this at runtime)
# ... (qcril_mbn_sw_load_to_db equivalent)
long_iin = iccid[:9] if iccid else None
short_iin = iccid[:6] if iccid else None
if long_iin:
row = db.execute("SELECT FILE FROM qcril_sw_mbn_iin_table WHERE MCFG_LONG_IIN=?",
[long_iin]).fetchone()
if row: return row[0]
if short_iin:
row = db.execute("SELECT FILE FROM qcril_sw_mbn_iin_table WHERE MCFG_IIN=?",
[short_iin]).fetchone()
if row: return row[0]
if mcc and mnc:
row = db.execute("SELECT FILE FROM qcril_sw_mbn_mcc_mnc_table WHERE MCC=? AND MNC=?",
[mcc, mnc]).fetchone()
if row: return row[0]
return "mcfg_sw/generic/common/WildCard/Wildcard/mcfg_sw.mbn" # fallback
```
**Note:** The `qcril.db` tables will be empty until we populate them. QCRIL populates them by
parsing each MBN binary's header (IIN/MCC-MNC metadata embedded in the MCFG blob). We need to
implement MBN metadata parsing to extract IIN and MCC-MNC lists from the MBN binary format to
build the DB. This is deferred — hardcoded Rogers is correct for this device and SIM.
### D.4 Solving the Timing Wall
> **CORRECTION 2026-06-20 (capture analysis — see `PAF/modem.md` frontier #4):** The "load +
> activate every boot, accept two restarts" model below is a *reconstruction of the unobserved
> first-provision path*, NOT what Android does in steady state. The captured Android radio logs
> (`android-live-20260619/deep/`) show steady-state boot is **verify-only**: `is_modem_mbn_updated`
> finds `prev_ver == cur_ver` and SKIPS the entire load/select/activate sequence; the modem
> self-applies MCFG persisted in its own EFS (`modemst`) at firmware RF-init. The full load path
> (and any `pdc_enable_auto_selection` call) runs only on first-provision / version change, which
> we have never captured. This section is retained as the design for the per-boot *workaround*
> (`tools/qcril-prov.py`, path A); the hardware-faithful target is provision-once + `modemst`
> persistence (path B). Resolve via the first-provision capture: `PAF/first_provision_capture.md`.
**The question:** Does the modem wait indefinitely for configs before RF-init, or does it have
a deadline?
**What we know:** PROGRESS.md 2026-06-20 states "PDC LoadConfig times out if attempted too
early (uptime ~45 s, modem not yet PDC-ready) but succeeds by ~226 s." This is the PDC-client
readiness window (when QRTR route is established). The RF-init window is a separate question.
**Hypothesis A:** The modem RF-inits as part of MPSS boot (~12 s) with whatever configs are
loaded. On cold pmOS boot there are none (0/0), so RF-init proceeds with no MCFG = no RF arm.
Provisioning after the fact cannot retroactively arm RF. The modem must be restarted (via
PDC ActivateConfig) after configs are loaded, which triggers a new RF-init pass WITH the
configs. This is the mechanism QCRIL uses.
**This matches all evidence:** PROGRESS.md 2026-06-20 states "A post-init userspace load cannot
arm an already-initialized RF, and nothing persists to carry configs to next boot." And:
"configs are PURELY VOLATILE, NOT persisted."
**Implication for our provisioner:** The timing wall is NOT about being early enough — it's
about triggering a PDC ActivateConfig (which causes modem restart + re-RF-init with configs
loaded). QCRIL's "early" timing is an optimization to reduce the number of restarts (load before
first RF-init = one restart; load after first RF-init = two restarts). Our provisioner will
cause two MPSS restarts per boot (once for HW, once for SW). This is acceptable for bring-up.
**The `--pdc-monitor-refresh` question** (PROGRESS.md 2026-06-20 "NEXT"): The modem may emit
a PDC refresh indication (`PDCRefreshIndication`, `qcril_qmi_pdc_refresh_ind_hdlr`) after
ActivateConfig, signaling completion. We should subscribe to this indication (via libqmi
`RegisterForPDCIndication`) to confirm activation before proceeding to the SW phase.
EVIDENCE: `"handleRegisterForPDCIndication(): failed with rc="` and
`"]: pdc refresh Completed"` strings.
**Recommended approach for `--pdc-monitor-refresh`:** Add a PDC indication registration at
startup and listen for `QMI_PDC_CONFIG_CHANGE_IND` (which is what `pdc_refresh_ind_hdlr`
handles). After each ActivateConfig, wait for the refresh-completed indication before polling
DMS. Timeout 30 s if indication doesn't arrive (fall through to DMS poll).
---
## E. Implementation Sequence (Recommended)
### Step 0 (validation experiment, before any code): confirm two-restart approach works
Manual test on device:
```sh
# Clean slate
qmicli -d qrtr://0 --pdc-list-configs=platform # should show 0
qmicli -d qrtr://0 --pdc-list-configs=software # should show 0
# Load + select + activate HW
python3 pdc_load.py qrtr://0 platform .../mcfg_hw.mbn
# get ID from pdc-list-configs=platform
python3 pdc_activate.py qrtr://0 platform <ID>
# wait ~30s for modem restart and DMS back
# Load + select + activate SW (Rogers)
python3 pdc_load.py qrtr://0 software .../mcfg_sw.mbn
# get ID from pdc-list-configs=software
python3 pdc_activate.py qrtr://0 software <ID>
# wait ~30s
# Check RF
qmicli -d qrtr://0 --dms-get-capabilities # expect Networks non-empty
```
This is the minimal end-to-end proof before building the daemon.
### Step 1: `qcril-prov.py` v1 (hardcoded)
Single script implementing the full D.2 flow with:
- Hardcoded HW MBN path (SDM845/LA/SS)
- Hardcoded SW MBN path (Rogers/CA)
- DMS poll loop (Stage 0)
- Delete-all existing configs (Stage 2)
- HW load/select/activate (Stage 3)
- Wait for modem restart (Stage 4, DMS poll again)
- SW load/select/activate (Stage 5)
- Verify Networks non-empty (Stage 6)
No carrier selection DB needed yet.
### Step 2: systemd integration
Wire the service unit (D.1), enable it, reboot, confirm RF arms without manual intervention.
### Step 3: add carrier selection (future, when needed for other SIMs)
Implement MBN binary parser for IIN/MCC-MNC metadata, populate a local sqlite3 DB, implement
`select_sw_mbn()` (D.3).
---
## F. Open Questions (evidence-bounded)
1. **Does the modem accept ActivateConfig before DMS is "fully" ready?** We know DMS must be
up for QCRIL to start — but does DMS-up precede or follow the RF-init window? Unknown without
`--pdc-monitor-refresh` experiment or timing capture.
2. **What is `pdc_enable_auto_selection(ii)` / `qcril_qmi_pdc_enable_modem_update`?** The
symbol `_Z25pdc_enable_auto_selectionii` exists in the binary. It may correspond to
`QMI_PDC_SET_FEATURE_VERSION` or a similar private PDC message that tells the modem to
accept auto-selected configs. If this is required, LoadConfig will succeed but ActivateConfig
may no-op. **Needs disassembly of `qcril_mbn_hw_update_init` to determine if it's called
before the load sequence.** This is the only remaining significant unknown.
3. **Does the Fido SIM (MVNO on Rogers) match the Rogers/CA MBN via MCC-MNC or IIN?** Fido's
MCC-MNC is 302-370; Rogers is 302-720. The Rogers MBN was active on Android with this Fido
SIM. So Rogers/CA is confirmed correct. The IIN or MCC-MNC lookup that produced this result
is unknown without the runtime-populated DB — but the outcome is known.
4. **Is the HW MBN name match on "SDM845-LA-SS" or another string?** `qcril_mbn_hw_get_hw_name_to_look_for`
returns the string used to match HW configs. The actual string is not visible without
disassembly. For blueline, only one SDM845/LA/SS entry exists in mbn_hw.txt; even if our
provisioner gets the name wrong, we can hardcode the file path.
---
## Sources
| File | Role |
|---|---|
| `android-reference/qcril-libs/libril-qc-hal-qmi.so` | Primary evidence: symbols (nm -D), strings |
| `android-reference/vendor-mbn/mbn/mbn_hw.txt` | HW MBN index (2 entries for SDM845) |
| `android-reference/vendor-mbn/mbn/mbn_sw.txt` | SW MBN index (55 entries, includes Rogers/CA) |
| `android-reference/vendor-mbn/mbn/mcfg.version` | MCFG version: `g845-00194-220517-B-8604334` |
| `android-reference/qcril-config/qcril.db` | DB schema; tables empty in prebuilt (QCRIL populates at runtime) |
| `android-reference/init-rc/qcrild.rc` | `class main`, confirms qcrild timing |
| `android-reference/init-rc/init.sdm845.rc` | Boot ordering; `rmt_storage` = `class core` (earlier than main) |
| `PAF/modem.md` | Eliminated leads; confirmed facts about PDC volatility and EFS |
| `PROGRESS.md` (2026-06-20 entry) | Decisive timing proof; QCRIL symbol inventory |
| `tools/pdc_load.py`, `tools/pdc_activate.py` | Our current implementation baseline |