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

37 KiB
Raw Permalink Blame History

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 SetSelectedConfigActivateConfig 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::handleDmsEndpointStatusIndMessageqcril_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_INITqcril_mbn_hw_update_init_hdlr: Initialize HW update state machine.
  2. REQUEST_MBN_HW_GET_SELECTED_CONFIGqcril_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_CONFIGqcril_mbn_hw_load_config_hndlr: Chunked QMI_PDC_LOAD_CONFIG for the selected HW MBN file.
  4. REQUEST_MBN_HW_SELECT_CONFIGqcril_mbn_hw_select_config_hndlr: QMI_PDC_SET_SELECTED_CONFIG for type=PLATFORM with the just-loaded config ID.
  5. REQUEST_MBN_HW_ACTIVATE_CONFIGqcril_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_INITqcril_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_CONFIGqcril_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_CONFIGSqcril_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_CONFIGqcril_mbn_sw_load_config_hndlr: Chunked QMI_PDC_LOAD_CONFIG for the selected SW MBN file (type=SOFTWARE).
  6. REQUEST_MBN_SW_SELECT_CONFIGqcril_mbn_sw_select_config_hndlr: QMI_PDC_SET_SELECTED_CONFIG for type=SOFTWARE.
  7. REQUEST_MBN_SW_ACTIVATE_CONFIGqcril_mbn_sw_activate_config_hndlr: QMI_PDC_ACTIVATE_CONFIG for type=SOFTWARE. Second modem restart.
  8. REQUEST_MBN_SW_CLEANUP_CONFIGqcril_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

[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=.
  4. Activate: QMI_PDC_ACTIVATE_CONFIG type=PLATFORM. Modem MPSS restarts.

EVIDENCE: full symbol chain qcril_mbn_hw_load_config_hndlrqcril_mbn_hw_select_config_hndlrqcril_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_hndlrqcril_mbn_sw_select_config_hndlrqcril_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):

# 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).


Step 0 (validation experiment, before any code): confirm two-restart approach works

Manual test on device:

# 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