Skip to content

Upstream currency: MM 1.24.2 stack + Wave-2/3 hardening#5

Merged
andrescera merged 19 commits into
mainfrom
feat/upstream-currency
Jul 17, 2026
Merged

Upstream currency: MM 1.24.2 stack + Wave-2/3 hardening#5
andrescera merged 19 commits into
mainfrom
feat/upstream-currency

Conversation

@andrescera

Copy link
Copy Markdown
Member

What

Brings the ModemManager packaging stack to current upstream and lands the Wave-2/Wave-3 hardening that rides with it. This is the single integration PR for the upstream-currency effort — 19 commits across packaging, CI, the transport seam, and docs. No package versions are bumped here (both package.json files stay at 0.1.0); the 0.2.0 cut is a separate release-prep PR that branches off this merge.

The upstream pin moves to ModemManager 1.24.2 / libmbim 1.34.0 / libqmi 1.38.0 / libqrtr-glib 1.4.0 (from the 1.24.0 / 1.32.0 / 1.36.0 / 1.2.2 era). Grouped by area:

  • Packaging (Wave 1). Re-pinned upstream-pins.yaml and refreshed all four debian/ trees to the new salsa revisions; re-derived the bookworm adaptations (the maintainers modernized the introspection build-deps to GI ≥ 1.80, which bookworm cannot satisfy — reverted to the libgirepository1.0-dev / GI-1.74 form, empirically proven sufficient); froze the per-source package sets and enforce exact set-equality on rebuild; added a shared read-pin.sh so CI and BENCH read one pin source textually, with revision-exact and direction-aware version assertions. Both arches rebuild with byte-exact package-set equality; the daemon smoke now does a functional GObject-introspection check (mmcli 1.24.2, typelib import, vala compile+link), not just a presence probe.
  • Provenance (Wave 1). verify-upstream-pins.sh gained a fourth chain link: the pinned .debian.tar tree is compared against the salsa packaging tag as a canonical manifest (type + exec-bit + symlink-target + content sha256), on top of the existing signer / .dsc-signature / .orig.tar checks. Four offline negative fixtures each reject on their specific named field.
  • Release CI (Wave 2). release.yml now pins every job checkout to the tag's peeled commit SHA, quarantines all workflow inputs through env: (zero ${{ }} in any run: body), sequences tag-guard → test → build-deb → publish-npm → create-release, makes the npm publish integrity-idempotent with a pre-publish TOCTOU re-check, and reconciles release assets immutably (existing assets are integrity-compared, never overwritten).
  • Transport seam (Wave 2). transport.ts was split into lifecycle / call-dispatch / signal modules (each ≤ 250 lines), behavior-locked by a new characterization suite committed and green before the split. The forbidden-surface detector was rebuilt on the TypeScript compiler API (replacing a regex), and the library-leak guard now enumerates modules dynamically.
  • Docs / policy (Wave 2/3). Dependabot ignores TypeScript semver-major bumps (TS 7 stays out of scope); Biome pinned to 2.5.4 with a reproducible lockfile; the FM350 decision is recorded as a tracked doc (PCIe identity → deferred, no support claim); and a full documentation sweep re-truths every version marker across the docs and packaging READMEs.
  • Test-harness fix (this PR). While running the pre-merge gate, dbus-run-session -- bun test intermittently exited 1 with a "Can't write a message to a closed stream" error attributed to an unrelated test. Root cause: the characterization suite's mid-drop test issues an in-flight SlowPing (5s reply), kills the bus, and deliberately abandons the fake — leaving a dangling reply timer that, when it fired seconds later, wrote to the closed stream. The final commit cancels the fake's reply timers when the connection stream ends (and in stop()), so a dead fake never writes. It changes no assertion; see the Rule-E note below.

Why

Upstream shipped the FM350 MTK-plugin fixes and a large run of library changes; staying on the old pins leaves the device image behind on both features and security fixes. The Wave-2/3 work is the hardening the plan gated on the same bump — provenance depth, injection-safe release automation, a maintainable transport seam, and honest docs — so it lands atomically with the pins rather than as drift afterward.

How to verify

All eight local gates were run on the merge head (f01b8e4) and exit 0:

Gate Result
bun install --frozen-lockfile exit 0, lockfile unchanged
dbus-run-session -- bun test 362 pass / 0 fail
bunx tsc --noEmit exit 0
bunx biome check . exit 0 (162 files)
bash packaging/ci/contract.sh exit 0
bash packaging/ci/test-tag-guard.sh exit 0 (only vX.Y.Z accepted)
bash packaging/ci/test-verify-upstream-pins.sh exit 0 (4 fixtures reject on named fields)
bash packaging/ci/verify-upstream-pins.sh exit 0 — live: 4 sources × 4 provenance links

The live provenance run verifies lineage + .dsc signature (GOODSIG in an isolated keyring) + checksums + .orig.tar + debian/ tree for all four sources. Fail-closed behavior is exercised, not assumed: the four verify fixtures reject on signer_fingerprint / dsc_signature / orig_tar_sha256 / debian_tree, and the tag guard rejects -rc.1, +build5, bare 1.0.0, v1.0, v1.0.0.0, and empty.

Rule-E justification — the three sanctioned test-file changes (each strengthens or is additive; none weakens)

  • control/src/transport/no-library-leak.test.ts — strictly stronger. The guard that asserts only the quarantined facade imports the D-Bus library was a fixed five-name list, each checked with .not.toContain. It now reads every non-test .ts in transport/ via readdirSync, collects the importers, and asserts the set is exactly ['dbus-native.ts']. The old list could never catch a new module it did not name — the split added calls.ts and signals.ts, exactly the case the fixed list was blind to. The file's other three tests are byte-unchanged (the only other edit is the readdirSync import).
  • control/src/ports/forbidden-surface.test.ts — strengthened, positives preserved. The regex member-name detector is replaced by a TypeScript compiler-API walk that inspects declared interface/type-literal/class members (including optional, quoted, computed-string, and accessor names). Every existing positive expectation still holds; the retired regex is kept in-file as legacyRegexMemberNames solely to power a differential test that runs both and proves the AST catches shapes (computed/quoted names, optional members, accessors, arrow-typed props) the regex silently missed. Real port files still produce zero false positives.
  • control/src/backend/features.test.ts — purely additive. One new case, parseMmVersion('1.24.2'){ major: 1, minor: 24 }, covering the new FM350-fix release. Zero existing lines touched.

Additionally, the harness fix in control/src/transport/test-support/fake-service.ts is not one of the three sanctioned test files and is not a test assertion — it is the fake D-Bus service double. Cancelling a dead connection's owed reply weakens nothing: the live-reply paths (characterization "late reply", reliability "late reply") are untouched because the new end-listener only fires when the bus is genuinely killed. A stash/restore before-and-after proof is in the run evidence (without the fix the repro exits 1 with three closed-stream hits; with it, clean).

Chronology anchor — characterization tests predate the split (Wave 2, task 2.3)

The transport refactor was done tests-first, and the ordering is preserved in genuine commit history:

  • 6de9d03test(transport): characterization coverage for reconnect/ordering edges (the characterization suite alone)
  • da0e37frefactor(transport): split transport.ts into lifecycle/calls/signals modules (behavior-locked) (the split)

6de9d03 precedes da0e37f in real commit order with real timestamps, preserved through this branch via --no-ff merges (not squashed). The GitHub PR commits listing for this PR shows 6de9d03 before da0e37f, which is the durable chronology anchor after this PR is squash-merged. Locally, the full transport suite (42/42) passed when only 6de9d03 existed — i.e. before any split module was written — which is the "tests characterize current behavior first" evidence. (The plan originally wanted a draft PR opened on 6de9d03 alone to capture that ordering as a CI-run anchor; the branch was instead assembled from parallel worktrees merged sequentially, so the commits-API ordering + the pre-split local pass stand in for that run.)

Risks

  • Packaging bump touches all four debian/ trees. Mitigated by the four-link provenance chain, verified live in this PR: signer fingerprint, .dsc GPG signature, .orig.tar sha256, and a byte-identical salsa-vs-.debian.tar tree manifest — plus a full amd64 + arm64 rebuild with exact per-source package-set equality (Wave-1 evidence). The debian/*.symbols files are salsa-faithful and were never regenerated.
  • libqrtr-glib install direction flipped below→above stock. At the old pin 1.2.2 matched bookworm stock and needed --allow-downgrades; at 1.4.0 it outranks stock, so the install is a plain upgrade. Verified empirically with real dpkg --compare-versions / apt-cache madison, and the contract suite is now direction-aware.
  • Transport split could change observable behavior. The characterization suite pins reconnect / call-correlation / event-ordering edges and stayed green across the split; the leak guard is now dynamic rather than a fixed list.
  • Release automation changed structurally. Not exercised end-to-end here (no tag is cut in this PR); it is proven by the 0.2.0 release drill in the follow-up. This PR only lands the workflow and its dependency-free helper scripts, all lint/actionlint clean.

…libqrtr-glib 1.4.0; canonical .dsc↔salsa tree link
… frozen package sets + external-dep preflight
Add ci/check-package-sets.sh: for each *.changes, assert its binary package
set EQUALS (not >=/count) the [<source> all-artifact] set in
expected-packages.txt, so any add/remove/rename fails closed naming the
package. Wire it into build-bookworm.sh after the runtime-closure check and
run it standalone per-arch.

Finalize expected-packages.txt all-artifact sets against the real amd64 and
arm64 *.changes (PROVISIONAL -> FINALIZED). The dbgsym enumeration inherited
from the debian/ refresh proved exact: a -dbgsym is emitted for exactly the 9
ELF-shipping packages and nothing else; no other delta on either arch.

build-bookworm.sh: install the freshly-built gir1.2-*-1.0 typelib packages
into the build environment before each dependent source. Bookworm's GI-1.74
dh_girepository resolves cross-namespace typelib deps (Qmi-1.0 imports Qrtr-1.0;
ModemManager-1.0 imports Qmi/Mbim/Qrtr) via the installed dependency .typelib,
and the rebuilt -dev packages carry an empty ${gir:Depends} under GI-1.74, so
libqmi otherwise died with "Could not find Qrtr-1.0.typelib dependency". This
is build-orchestration only (mirrors an archive build where every gir1.2-* is
co-installable) and changes no produced package's contents or the emitted set.

Document both in packaging/README.md.
…-publish, integrity-idempotent npm, staged sanitized assets
A reconnect/drop test kills the private bus and abandons the fake service
without stopping it. A SlowPing whose reply delay had not yet elapsed left a
dangling timer that, when it fired, wrote the owed reply to the closed stream
and threw "Can't write a message to a closed stream" asynchronously — seconds
later, inside an unrelated test file — flipping the run's exit code to 1 while
no test actually failed.

Track the reply timers and cancel them the instant the connection stream ends
(and in stop()), so a dead fake never writes. The live-reply paths are
untouched: 'end' only fires when the bus is genuinely killed.
@andrescera
andrescera merged commit 9a9b640 into main Jul 17, 2026
2 checks passed
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