Skip to content

Runtime USB-composition capability, evidence-selected ZTE login, UFI descriptor capture, and the FM350-GL carry - #19

Merged
andrescera merged 12 commits into
mainfrom
modem-control-ui-parity
Aug 24, 2026
Merged

Runtime USB-composition capability, evidence-selected ZTE login, UFI descriptor capture, and the FM350-GL carry#19
andrescera merged 12 commits into
mainfrom
modem-control-ui-parity

Conversation

@andrescera

Copy link
Copy Markdown
Member

Affected repo & language: modem-stack — TypeScript (control/, cli/) + Debian packaging (packaging/)

What

Three independent strands of modem work, plus the FM350-GL packaging exception they
sit alongside.

Runtime USB-composition capability. The ModemManager provider used to decide which
USB composition modes it could offer by looking the device up in a model catalog. It now
asks the device, through the exact reviewed vendor READ/TEST forms already in the AT
allowlist, and offers only the targets that device's own enumeration reports — and only
when that enumeration also contains the device's current mode, which is what proves a
return path exists. A catalog miss is no longer an uncertified verdict. The operator-facing
suppression vocabulary is exactly four literals (unknown-vendor, no-return-path,
blocked-by-state, provisioning-disabled), and every suppressed state offers zero targets.
Success keeps two proof tiers: a reviewed catalog transition still requires both the
canonical mode and the USB descriptors to match (tier 1, unchanged and still strongest);
without one, the re-enumerated device must report the target through its own vendor READ
(tier 2, explicitly weaker). AT OK is proof in neither. Band writes do not share this
policy and remain behind their separate four-proof certification gate.

ZTE login-encoding selection. The goform provider chose its password encoding from
firmware text, which cannot distinguish the MF79U B03 LD-salted dialect from the legacy
base64 one. It now reads a batched pre-auth multi_data GET — LD, remaining attempts,
lock time, both version fields — and selects the algorithm from that evidence, refusing a
positively-reported lockout before any credential POST is sent. One bounded attempt per
selection, and no fall-through to a neighbouring algorithm.

UFI/HIMI descriptor capture tooling. A read-only bench capture script and its bundle
schema, interface-role classifier, and independent redaction sweep, for the 05c6:9091
stick. Redaction happens at capture time with one implementation (the script's own
--redact-filter mode, which the test executes rather than re-expresses); the staged bundle
is swept before publication and a surviving identifier destroys it with no override flag.
No bundle content is committed. The 2026-08-23 hardware drill's classification is recorded
in docs/UFI-DIAG-PROBE.md: a four-interface QMI + ADB-class composition, interface 2 claimed
by qmi_wwan, no ff/ff/30 DIAG descriptor, HIMI identity endpoint unreachable through
the target's own wwan1. No composition change was attempted.

The FM350-GL patch series. Three BELABOX-derived quilt patches on ModemManager 1.24.2 —
the forward-ported fm350gl plugin, the +CPOL crash avoidance, and the extended +COPS
scan format — carried under the narrow owner-approved exception recorded in POLICY.md §1
and docs/adr/ADR-FM350-RNDIS-BEARER.md. Also here: the fix that made them actually work
(e9d41bc), the hardware validation, and the docs recording it.

Why

The composition-catalog change is the substantive one. Gating an interrogable device's
USB-mode switching on a model catalog means the catalog is a de-facto allowlist: a modem
that can tell you exactly which compositions it supports gets hidden because nobody has
reviewed a row for it yet. That is the "uncertified means unsupported" coercion the
support-claim taxonomy exists to prevent. Deriving targets from the device's own
enumeration keeps the catalog valuable as evidence — it still provides the strongest
success proof — without letting it decide whether a control is offered at all. The
return-path requirement is what keeps this safe: a device is offered a switch only when its
own vocabulary demonstrably includes the mode it is currently in, so there is a way back.

The ZTE change fixes a real misdiagnosis. Two MF79U firmwares share a version string and
disagree about the password algorithm, so encoding-from-firmware-text guesses, and a wrong
guess spends one of a small number of login attempts. Reading the lockout state first means
we stop attempting rather than burning the last attempt and locking the device out.

On the FM350 series: the first hardware drill failed, and the failure was ours. The
1.24 forward-port had dropped BELABOX's modem-class first-enable override, so core
ModemManager sent ATZ where the original plugin sent ATZ0; the modem answered CME 59
and no bearer could ever open. e9d41bc restores the override. With it, the corrected
series built for arm64 and passed the full criterion on the carrier-mounted unit:
nmcli connection up succeeded, registration home, packet service attached, a routable
IPv4 on the RNDIS interface, and curl --interface enx000011121314 https://example.com
returned HTTP 200.

Two things about that result are deliberately not claimed. It validates the
carrier-mediated USB topology and that firmware — it is not a PCIe / mtk_t7xx
support claim, and docs/FM350-DECISION.md's three-gate ledger stays open. And
POLICY.md §1 requirement 2 (a filed upstream MR) is still honestly not met. The MR is
drafted at docs/adr/FM350-UPSTREAM-MR-DRAFT.md and its filed-MR table reads _not filed_
in both rows. What exists is the project owner's dated second-maintainer approval to carry
the series while that filing action stays open — an explicit exception for this one series,
not a general waiver and not a claim that an MR exists. Filing it remains an open follow-up;
the ADR §7 gate table and POLICY.md both say so in as many words, and this PR does not
relabel that.

How to verify

Workspace gates, from the repo root:

bun install
bun run lint          # biome check .
bun run typecheck     # tsc --noEmit (strict + exactOptionalPropertyTypes)
bun test              # 1378 tests / 119 files

cd control
bun run verify:tarball
CERALIVE_NODE_BIN=/path/to/node26 bun run verify:consumers

verify:consumers refuses any Node major but 26 on purpose — a green result cannot come
from an older Node that happens to be first on PATH.

Packaging, the lightweight PR lane (no docker-in-docker, no built .deb):

docker run --rm -v "$PWD:/w" -w /w debian:bookworm bash -c '
  apt-get update && apt-get install -y --no-install-recommends \
    git ca-certificates devscripts dpkg-dev ccache &&
  git config --global --add safe.directory /w &&
  bash packaging/ci/contract.sh'

That lane includes test-fm350-patch-contract.sh, which is the gate on this series: it
asserts each patch retains its BELABOX author, originating commit SHA, rationale and
Forwarded: no status, so the attribution cannot silently decay into an anonymous
downstream fork.

The heavy, .deb-consuming lanes need a build first (packaging/ci/build-bookworm.sh amd64);
both were run locally against that output and pass:

bash packaging/ci/test-package-contract.sh amd64   # 8 checks, mode=full
bash packaging/ci/daemon-smoke.sh                  # amd64

The contract suite's upgrade table is worth a look — every source sorts above stock
bookworm with real dpkg --compare-versions, so the upgrade needs no --allow-downgrades,
and the rollback to stock is clean. The smoke run brings the daemon up on a live bus and
confirms Version 1.24.2, mmcli 1.24.2, the ObjectManager root path, the udev and
FCC-unlock install paths, and a real PyGObject typelib import plus a Vala compile-link
against libmm-glib.

For the FM350 bearer claim specifically, the hardware transcript is in the ADR; it cannot be
reproduced without the carrier board and a live SIM.

Risks

The composition change widens what gets offered, which is the point but is also the
risk: a device that previously fell through to uncertified may now be offered targets on
tier-2 proof only. The fences are the return-path requirement (no offer unless the device's
own enumeration contains its current mode), the four-literal suppression vocabulary, and the
unchanged write-side fences — admission lease, journal, armed rollback, required readback,
fail-closed identity, streaming interlock, MM inhibit/uninhibit, and the bounded
drop/re-enumeration wait. The AT fence widened by exact name, never by pattern: capability
reads send only allowlisted READ/TEST forms and no SET, and only the selected enumerated
target is unioned into the held lease.

The FM350 series is the one carried patch set in a no-fork repo, so it is a standing
merge liability against ModemManager's device database. It is pinned, attributed, contract-
gated, and must be retired when upstream ships equivalent support. Its validation covers one
topology and one firmware; PCIe remains unproven and ungated by this PR. Rollback is
removing the three series entries — the four sources are otherwise source-unmodified, and
the contract suite proves a clean downgrade to stock.

The ZTE evidence probe adds one pre-auth GET before login. It is unauthenticated and
read-only, and it strictly reduces credential attempts: it can only refuse an attempt that
would previously have been made blindly.

The UFI tooling touches no device. It is bench-only, unpublished (files: ["dist"]),
and lives in control/scripts/ rather than the provider directory precisely because
no-write-path.test.ts asserts that directory's listing as an exact literal. A static gate
scans both files for usb_modeswitch, setprop, shell-transport invocation, EDL tooling,
AT!, uppercase AT write forms and QMI writes, each with a non-vacuity control.

No npm publish, no .deb upload, no apt-worker dispatch, and no tag are part of this PR.


Checklist

  • Docs updated if behavior or structure changed (Rule A: AGENTS.md, README, docs/)
  • Started from updated main; branch rebased on latest canonical branch (Rule B)
  • Rule D local-scratch reference check passes for tracked files
  • Tests pass; QA evidence attached or linked

… schema

Bench tooling for the `05c6:9091` UFI stick: a capture script that writes a
redacted descriptor evidence bundle, a schema/classifier module that says what a
bundle is and what its interfaces are, and a gate that proves neither can change
the device. No bundle content is captured here — that is the hardware drill's
job, and no device is needed to land this.

The tooling lives in `control/scripts/`, not in the provider directory. The
provider's `no-write-path.test.ts` asserts that directory's listing as an exact
literal, and bench tooling is not part of the published package (`files:
["dist"]`) — `mf79u-diagnose.sh` is the standing precedent. The module still
imports `classifyUfiDiagEvidence`, so the bench DIAG rule cannot drift from the
shipped one.

Redaction happens at capture time and the rules have exactly one implementation
— the script's `--redact-filter` mode, which the test executes rather than
re-expresses. Two layers mirroring `redact.ts`: key-based masking of serial,
IMEI, IMSI, ICCID, MSISDN, token and password fields, plus a MAC and 14+-digit
backstop. Nothing in a USB descriptor is 14 digits long, so class triples,
`bcdDevice`, driver names and product strings survive verbatim. The staged
bundle is swept before publication and a surviving identifier destroys it; there
is no override. Staging is moved into place as a unit, so a published path
either holds a complete bundle or does not exist, and a host with no matching
device answers `device-not-present` having written nothing.

The descriptor triple and the driver binding stay two separate facts. Upstream
matches `05c6:9091` in `qmi_wwan.c` under an annotation naming a different
device and QCSuper documents the id on another one again, so only this unit's
own descriptor answers — and only the captured binding says who claimed it.
@andrescera
andrescera merged commit e9f61c5 into main Aug 24, 2026
2 checks passed
@andrescera
andrescera deleted the modem-control-ui-parity branch August 24, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant