Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/saf/device/evidence/qcril_online_sequence.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

14 KiB

QCRIL pre-online QMI choreography — extracted from libril-qc-hal-qmi.so

Binary: android-reference/vendor-stack-20260621/vendor/lib64/libril-qc-hal-qmi.so (ARM64, stripped of locals but full dynamic symbol table present — 22045 syms, qcril function names intact). Decompiler unavailable (no rz-ghidra / pdg); extraction is from disassembly of named functions + QMI message-id immediates + the log-string call flow + the working-Android logs in android-capture-20260621/diag/.

All addresses are file offsets into the .so .text. Disassembly cited inline.


0. What the binary settles (resolves the doc contradiction)

PAF/qcril_bringup_capture.md ends with a REFRAME claiming "the Android modem self-onlines; there is no QCRIL online handshake." The binary shows that is half right: QCRIL does issue DMS Set Operating Mode, but only as the last step of a gated state machine (qcril_qmi_nas_boot_up_optimization_state_machine @ 0xfe4730). The thing pmOS skips is not "an online command" — it is the pre-conditioning QCRIL performs on the modem before it asks it to go online. That pre-conditioning is the choreography below.

The "self-online" observation on Android (op-mode already 0 at QCRIL attach) is because on a warm / SSR path the modem was already conditioned in a prior boot and the state persisted; the cold first-boot path runs the full state machine. pmOS only ever does the cold bare dms-set-operating-mode=online with none of the pre-conditioning → DeviceNotReady(52).


1. The bring-up orchestrator — qcril_qmi_nas_boot_up_optimization_state_machine @ 0xfe4730

Disassembled call order (filtered to the load-bearing bls):

fe4850  bl qcril_qmi_nas_get_bootup_power_optimization_state
fe4880  bl qcril_qmi_nas_decide_mode_pref_to_set_on_modem(&pref)   ; 0xfe4c00
fe489c  bl qmi_ril_nwreg_request_mode_pref(i, &pref)               ; 0x10f6f98  <-- NAS mode-pref push
fe48cc  bl qcril_qmi_nas_save_android_mode_pref(i)
fe48d8  bl qcril_qmi_nas_get_atel_ui_ready_req_came_from_cache()   ; 0xfe4ec4
fe48f0  bl qcril_qmi_nas_set_atel_ui_status_to_modem(h)            ; 0xfe5070  <-- DMS UI-ready push
fe48f8  bl qcril_qmi_nas_set_atel_ui_status_to_cache(h)
fe4904  bl qcril_qmi_nas_set_operating_mode_state(enum)            ; 0xfe53c8  <-- DMS Set Operating Mode
fe490c  bl qcril_qmi_nas_set_bootup_power_optimization_state(2)

So the ordered, load-bearing pre-online sequence the modem is conditioned with is:

  1. Decide + push the NAS mode preference (RAT set) — tell the modem which radio technologies to acquire — BEFORE asking it to power the RF.
  2. Push the ATEL "UI ready" status to the modem over DMS — a Qualcomm OEM DMS message.
  3. Only then issue DMS Set Operating Mode.

qcril_qmi_nas_telephony_ready @ 0x1015a50 carries the log poweron_opt: 'UI ready' NOT sent to modem (string confirmed in binary) — i.e. the UI-ready push is conditionally skipped when the modem is already online, which is exactly the "self-online" warm path that misled the earlier reframe.


2. Step detail + QMI message IDs (from the immediates)

Step A — NAS mode preference (LOAD-BEARING)

qmi_ril_nwreg_request_mode_pref @ 0x10f6f98 builds the mode/band/acq-order prefs (qcril_qmi_nas_get_mode_pref @0x1da4890, qcril_qmi_nas_get_gw_acq_order_pref @0x1da4d50, qcril_qmi_nas_get_5g_mode_pref @0x1da3940) and dispatches through the shared sender at qcril_qmi_nas_request_set_preferred_network_type+0x340 (0x10f6f68 region). That sender issues NAS Set System Selection Preference (QMI NAS id 0x0033, confirmed present in references/libqmi/data/qmi-service-nas.json line 1437 "Set System Selection Preference"). → Replayable via qmicli --nas-set-system-selection-preference.

Step B — ATEL UI-ready push (LOAD-BEARING, NOT replayable with stock tooling)

qcril_qmi_nas_set_atel_ui_status_to_modem @ 0xfe5070:

fe5124  mov  w0, #0x4                 ; qcril client enum 4 = DMS
fe5128  bl   qcril_qmi_client_get_user_handle
fe512c  mov  w1, #0x64               ; <-- QMI message id 0x64 (100) on DMS
fe5134  mov  w3, #0x2                ; req_len = 2 bytes (the UI-ready u8 + TLV hdr)
fe5140  mov  w5, #0x8                ; resp_len = 8
fe5148  movk w6,#0x1,lsl#16 / 0x2110 ; sync timeout
fe514c  bl   qmi_client_send_msg_sync_with_shm

DMS message 0x0064 is a Qualcomm OEM extension (string com.qualcomm.qti.qcril.nas.telephony_ui_ready) and is not defined in libqmi (checked references/libqmi/data/qmi-service-dms.json — 0x0064 absent; only 0x002E Set Operating Mode etc.). There is no qmicli flag for it and no Qmi.MessageDms* GI binding. Sending it requires a hand-built raw QMI PDU via qmi_device_command_full. See gap note in §4.

Step C — DMS Set Operating Mode (the step pmOS already does)

qcril_qmi_nas_set_operating_mode_state @ 0xfe53c8:

fe547c  mov  w0, #0x4                 ; DMS client
fe5484  mov  w1, #0x2e               ; QMI DMS id 0x2E = Set Operating Mode (libqmi dms.json:498)
fe548c  req @ x29-0x18, len 0x10
fe549c  movk ... 0x12110 timeout
fe54a4  bl   qmi_client_send_msg_sync_with_shm

The op-mode enum value comes in as the function argument (dms_operating_mode_enum_v01); online = 0. → This is exactly qmicli --dms-set-operating-mode=online.


3. Surrounding init context (incidental vs load-bearing)

qcril_qmi_nas_init @ 0xfc7058 also runs, at NAS bring-up (de-mangled call trace):

  • qcril_qmi_nas_configure_sig_info2 — signal-strength delta config (incidental to RF arm)
  • qcril_qmi_util_enable_networking_indications(int,int,int) — NAS indication-register (sys-info / serving-system). NAS Indication Register = QMI NAS 0x0003 (qmi-service-nas.json:395 "Register Indications"). Replayable, but incidental to RF arming — it only controls which unsolicited indications the modem pushes back; it does not gate op-mode. Useful so we can observe the modem reacting.
  • qcril_qmi_nas_register_for_5g_modem_vote_indication, sig-config — incidental.

UIM path (separate module, runs from card-status-update, not the op-mode state machine):

  • qcril_qmi_uim_power_up @ 0x11dc650 (MMGSDI card power-up) and qcril_qmi_uim_change_provisioning_session @ 0x11e07e0. On Android these establish the USIM session before NAS asks for RF. On pmOS the SIM already reads ready and primary-GW is provisioned (per live results), so card power-up is already satisfied — not the gap. uim-change-provisioning-session timing out on pmOS is a symptom of the same modem not-ready state, not an independent cause (it answers reads, refuses state changes).

qmi_ril_nwreg_post_oprt_online_action_handler @ 0x104ddd8 runs after online succeeds (enables more indications) — not part of the pre-online arm.

Load-bearing-for-RF-init, in order (the replay target)

  1. NAS Set System Selection Preference (mode/RAT pref) — 0x0033, qmicli-able
  2. DMS ATEL UI-ready push — DMS 0x0064, OEM, not qmicli-able
  3. DMS Set Operating Mode = online — 0x002E, qmicli-able

Incidental (safe to include for observability, not expected to gate)

  • NAS Register Indications (0x0003), sig-info config
  • UIM card power-up / provisioning (already satisfied on pmOS)

4. Precise gap (what the binary requires that stock pmOS tooling can't send)

DMS message 0x0064 ("telephony UI ready") is the one pre-online step QCRIL performs that neither qmicli nor libqmi-GI exposes. It is a 2-byte request on the DMS service. We can: (a) replay steps 1 + 3 with qmicli now (cheap, may be sufficient if the mode-pref is the real missing precondition), and (b) if 1+3 still give 52, send 0x0064 as a raw QMI PDU via Qmi.Device.command_full (the TLV layout is a single mandatory u8 = 1; we have the message id and req_len=2 from the binary, which is [type=0x01][len=0x01 0x00][val=0x01] = 4 bytes payload after the QMI msg header — note binary's "req_len 2" counts the c-struct, libqmi will frame the TLV).

The provisioner (tools/modem-online-provisioner/) implements (a) as the primary path and documents (b) as the staged escalation, so we spend device boots in the right order.

UPDATE 2026-07-01 — (b) is now built AND run on hardware. Result: real response, still 52. device-google-blueline/dms-ui-ready.py sends the raw 0x0064 PDU using libqmi's own untyped raw-message primitives (qmi_message_new + qmi_message_tlv_write_init/guint8/complete + qmi_device_command_full). First attempt (Python/GI) failed immediately: Qmi.Message is NOT exposed via GObject-Introspection in this build — confirmed live (AttributeError: 'gi.repository.Qmi' object has no attribute 'Message') and by enumerating the actual typelib (only per-message typed classes exist, e.g. MessageDmsGetOperatingModeOutput; no generic Message/raw-Command type). The doc-comment annotations on qmi_message_new/ qmi_message_tlv_write_* in the C headers do NOT mean GI exposes them — they don't here.

Second attempt (C, direct against libqmi's real C API) worked and got a genuine protocol response. Cross-compiled a small standalone binary (pmbootstrap chroot -b aarch64, libqmi-dev 1.39.0 — the openimsd fork, matching the phone's installed version) linking directly against libqmi-glib.so.5 already on the phone; pushed and ran it live:

dms-ui-ready: sending DMS 0x0064 (telephony UI ready=1)
dms-ui-ready: response: is_response=1 message_id=0x0064 length=20
dms-ui-ready: RESULT: FAILURE (result=1 error=58)

The modem answered 0x0064 as a real, understood message (not "unsupported message" / not a transport reject) — this alone is new information: the OEM message ID and TLV framing are correct. It returned a structured FAILURE with error code 58. Standard libqmi enum 58 is QMI_PROTOCOL_ERROR_WMS_ENCODING (an SMS-service error) which cannot be its real meaning here — this is a Qualcomm OEM message, so its error-code namespace is very likely private/vendor-defined and just numerically reuses slot 58; not confidently decodable from public libqmi headers. Retried mmcli --enable immediately after (on both the pre-existing and, after ModemManager's own transport reprobe, a freshly re-created modem object): DeviceNotReady (52), byte-identical to every prior attempt all night, with or without this message. Sending it also correlated with a transient QMI transport hiccup (ModemManager's own connection self-recovered via reprobe within ~1s; no lasting harm, but note for next time: release the DMS client / close the device cleanly before exit, which this quick test skipped).

Verdict: DMS 0x0064 is real, reachable, and rejects with a specific (undecoded) error — but does not clear the 52 wall by itself. Either the TLV value/precondition isn't exactly right, or this step genuinely isn't sufficient alone and step 1 (NAS mode-preference, qmicli --nas-set-system-selection-preference, previously observed to fail with DeviceUnsupported pre-RF) needs to succeed first, or the real gate is elsewhere entirely (EFS NV item, bootloader/ ABL verified-boot state — see los-mss-translated.c's remaining two candidates). Kept wired in the boot chain (best-effort, harmless) since it's now proven to do something real, just not (yet, alone) the fix.

UPDATE 2026-07-02 — full ordered trilogy executed on hardware. Candidate #2 CLOSED. Run conditions: ModemManager stopped, fresh mss restart, first commands into the new firmware instance, QCRIL's exact order. Results:

  1. NAS 0x0033 (mode-pref) → DeviceUnsupported(25), identical to every out-of-order attempt. Order does not rescue it. But --nas-get-system-selection-preference succeeds and shows the stored pref is ALREADY complete (all RATs, full band lists, persisted from Android) — step 1's content is pre-satisfied; the push is refused AND redundant.
  2. DMS 0x0064 (UI-ready) → delivered, NoEffect(26), as always.
  3. DMS 0x002E online → DeviceNotReady(52), byte-identical. Same night context: rfs file serving fully fixed (ota_firewall/ruleset + modem_efs; XPU-violation boot crashes eliminated), zero TQFTP rejects, clean crash-free modem boot. The pre-online conditioning theory is now closed in ALL variants: the modem is conditioned, fed, and healthy, answers every read, refuses every state change. Remaining candidates: EFS NV item (#3), ABL/bootloader-passed state (#5), with the TZ/SFS secure-storage identity path (test IMEI 990012001581294) as the concrete mechanism consistent with both. Next experiment queued: QSEECOM listeners started pre-modem at cold boot (registration proven; receive/resume relay still unexercised).

UPDATE 2026-07-03 — DMS 0x0064 NOT_READY-then-READY handshake tested live. FALSIFIED. Real vendor QCRIL source (leaked SDM670/710 BSP, qcril-hal/modules/nas/src/qcril_qmi_nas.cpp, boot-triggered-ONLINE branch of the unsolicited operating-mode indication handler) shows the real sequence sends ui_status=FALSE (NOT_READY) first, then later TRUE (READY) — our prior test (2026-07-01) only ever sent READY standalone. Built a corrected C tool (tools/modem-online-provisioner/dms-ui-ready-sequence.c, cross-compiled via pmbootstrap chroot -b aarch64 against a sysroot pulled live from the phone's own /usr/include/libqmi-glib + .so, since neither glib-dev nor libqmi-glib-dev are in the buildroot's own repo index) and ran the full corrected sequence live on pmOS: baseline offline confirmed, 0x0064(0) sent → result=1 error=58, 0x0064(1) sent 1s later → result=1 error=58 (identical failure, order didn't matter), final dms-get-operating-mode read → still offline. Both values fail identically — this rules out "wrong value" or "wrong order" as the cause of error 58. Error 58 is very likely a hard precondition-not-met rejection independent of what value is sent, not a sequencing bug. Do not re-attempt this specific NOT_READY/READY ordering fix without new information; it's closed. Tool kept in tools/modem-online-provisioner/ for reference (working example of raw libqmi C message construction against DMS OEM messages, useful if a future OEM message needs the same treatment).