aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
#!/bin/sh -u
|
|
|
|
|
# fp6-vendor-blobs-extract - copy proprietary blobs out of the stock Android
|
|
|
|
|
# partitions instead of distributing them. pmOS installs flash only
|
|
|
|
|
# boot+userdata, so every installed FP6 still carries the stock vendor/dsp
|
|
|
|
|
# partitions: the device duplicates files it already contains, for its own
|
|
|
|
|
# operation - nothing is distributed by us or anyone else. Design, legal
|
|
|
|
|
# frame and on-phone verification: fp6 repo journal/blobs/.
|
|
|
|
|
#
|
|
|
|
|
# Manifest fragments: /usr/share/fp6-vendor-blobs/manifest.d/*.manifest,
|
|
|
|
|
# processed in sorted order; '#' comments and blank lines ignored:
|
|
|
|
|
#
|
|
|
|
|
# file <partition[,partition...]> <path-in-partition> <dest> <sha256>
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
# mbn <partition[,partition...]> <dir-in-partition> <name> <dest> <sha256|->
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
# rebind <bus> <device>
|
|
|
|
|
#
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
# Partition lists are tried in the order written, except that on an A/B
|
|
|
|
|
# device the ACTIVE slot's partitions (androidboot.slot_suffix in
|
|
|
|
|
# /proc/cmdline) come first: the other slot may hold a different Android
|
|
|
|
|
# build, and for a signed image only the active slot's copy is guaranteed to
|
|
|
|
|
# match the TZ that is running.
|
|
|
|
|
#
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
# file: mount the first available listed partition READ-ONLY (ext4 also
|
|
|
|
|
# gets -o noload - never a byte written to the stock partitions, not
|
|
|
|
|
# even a journal replay), copy <path-in-partition> to <dest>, verify the
|
|
|
|
|
# sha256. A missing source or a hash mismatch tries the next listed
|
|
|
|
|
# partition; no verified copy on any of them fails the run loudly - an
|
|
|
|
|
# unverified blob is never installed and a missing one never silently
|
|
|
|
|
# skipped. Dests that already exist are left alone (no hashing: a
|
|
|
|
|
# deliberately replaced file stays).
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
# mbn: the same, for a Qualcomm trustlet, which is not shipped as one file.
|
|
|
|
|
# QTEE images live in the modem partition's image/ as an ELF header+hashes
|
|
|
|
|
# file (<name>.mdt) plus one payload per program header (<name>.b00, .b01,
|
|
|
|
|
# ...), and the loader wants them written back at each segment's p_offset.
|
|
|
|
|
# Reassembly is therefore not a concatenation: segments are page aligned
|
|
|
|
|
# but not contiguous, gaps stay zero, and two segments may share an offset
|
|
|
|
|
# (focal64 has two such pairs), so they are written in index order and the
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
# later one wins. With a real sha256 the guarantees are file's: the hash is
|
|
|
|
|
# of the reassembled image, a mismatch tries the next partition, an
|
|
|
|
|
# unverified image is never installed. With '-' the image is verified
|
|
|
|
|
# STRUCTURALLY instead - ELF64 header, every segment present at the size
|
|
|
|
|
# its program header declares, page-aligned offsets, sane total - and not
|
|
|
|
|
# against a pinned hash. That is the right mode for an OEM-signed trustlet:
|
|
|
|
|
# the OEM re-signs it every Android release, so one whole-image hash matches
|
|
|
|
|
# exactly one build (six builds, six hashes, one trustlet: fp6 repo
|
|
|
|
|
# journal/fingerprint/ 2026-09-07..11, two field units failed on the pin),
|
|
|
|
|
# while the loader in TZ verifies the signature and the per-segment hashes
|
|
|
|
|
# itself and refuses a damaged or foreign image (one flipped byte ->
|
|
|
|
|
# ERROR_ELF_SIGNATURE_ERROR, measured 2026-09-03). The sha256 of what was
|
|
|
|
|
# installed is logged either way.
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
# rebind: if this fragment's run extracted at least one file, unbind and
|
|
|
|
|
# re-probe <device> on <bus> so the consuming driver picks the file up
|
|
|
|
|
# in the same boot. Unconditional on purpose: a still-bound consumer may
|
|
|
|
|
# have already failed a deferred firmware request that is never retried
|
|
|
|
|
# (aw88261 binds on i2c probe but requests the ACF only at card init),
|
|
|
|
|
# so only a fresh probe with the file present is a known-good state.
|
|
|
|
|
# All of a fragment's devices are unbound first, then re-probed, so a
|
|
|
|
|
# shared sound card re-forms once instead of bouncing per device.
|
|
|
|
|
#
|
|
|
|
|
# Partitions resolve via /dev/mapper (the pmOS initramfs maps the dynamic
|
|
|
|
|
# partitions in super on every boot), then /dev/disk/by-partlabel (raw
|
|
|
|
|
# partitions like dsp_a), then one make-dynpart-mappings fallback run;
|
|
|
|
|
# mappings that run creates are removed again at the end.
|
|
|
|
|
#
|
|
|
|
|
# --if-device: exit 0 quietly when no stock super partition is visible
|
|
|
|
|
# (apk post-install scripts run inside build/CI chroots too; on images
|
|
|
|
|
# built there the first-boot service does the real extraction).
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
# --refresh: re-derive every mbn dest even if it exists, replacing it only
|
|
|
|
|
# with an image that verifies (a failed refresh leaves the old file). For
|
|
|
|
|
# the consumer's post-install/post-upgrade: a fresh 'apk add' gets its
|
|
|
|
|
# trustlet without a reboot, and a trustlet that was hand-placed or pinned
|
|
|
|
|
# to another build is replaced by the active slot's on the next upgrade.
|
|
|
|
|
# file dests are still left alone: re-copying the acf would rebind the
|
|
|
|
|
# sound card on every upgrade for nothing.
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
|
|
|
|
|
MANIFEST_DIR=/usr/share/fp6-vendor-blobs/manifest.d
|
|
|
|
|
SUPER=/dev/disk/by-partlabel/super
|
|
|
|
|
MNT=
|
|
|
|
|
MNT_PART=
|
|
|
|
|
CREATED=
|
|
|
|
|
TRIED_MAPPING=
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
IF_DEVICE=
|
|
|
|
|
REFRESH=
|
|
|
|
|
# "a" or "b" on an A/B device (androidboot.slot_suffix=_a), else empty
|
|
|
|
|
ACTIVE_SLOT=$(tr ' ' '\n' </proc/cmdline 2>/dev/null | sed -n 's/^androidboot\.slot_suffix=_\([ab]\)$/\1/p' | head -n1)
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
|
|
|
|
|
log() { echo "fp6-vendor-blobs: $*"; }
|
|
|
|
|
|
|
|
|
|
unmount_cur() {
|
|
|
|
|
if [ -n "$MNT" ]; then
|
|
|
|
|
umount "$MNT" 2>/dev/null
|
|
|
|
|
rmdir "$MNT" 2>/dev/null
|
|
|
|
|
MNT= MNT_PART=
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
|
unmount_cur
|
|
|
|
|
for name in $CREATED; do
|
|
|
|
|
dmsetup remove "$name" 2>/dev/null || true
|
|
|
|
|
done
|
|
|
|
|
CREATED=
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fail() {
|
|
|
|
|
echo "fp6-vendor-blobs: ERROR: $*" >&2
|
|
|
|
|
cleanup
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# resolve a partition name to a block device
|
|
|
|
|
part_dev() {
|
|
|
|
|
[ -b "/dev/mapper/$1" ] && { echo "/dev/mapper/$1"; return 0; }
|
|
|
|
|
[ -b "/dev/disk/by-partlabel/$1" ] && { echo "/dev/disk/by-partlabel/$1"; return 0; }
|
|
|
|
|
if [ -z "$TRIED_MAPPING" ] && [ -b "$SUPER" ]; then
|
|
|
|
|
TRIED_MAPPING=1
|
|
|
|
|
before=$(dmsetup ls 2>/dev/null | awk '{print $1}')
|
|
|
|
|
make-dynpart-mappings "$SUPER" 0 2>/dev/null
|
|
|
|
|
after=$(dmsetup ls 2>/dev/null | awk '{print $1}')
|
|
|
|
|
for name in $after; do
|
|
|
|
|
case " $before " in *" $name "*) ;; *) CREATED="$CREATED $name" ;; esac
|
|
|
|
|
done
|
|
|
|
|
[ -n "$CREATED" ] && log "mapped dynamic partitions:$CREATED"
|
|
|
|
|
[ -b "/dev/mapper/$1" ] && { echo "/dev/mapper/$1"; return 0; }
|
|
|
|
|
fi
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mount_part() {
|
|
|
|
|
[ "$MNT_PART" = "$1" ] && return 0
|
|
|
|
|
unmount_cur
|
|
|
|
|
dev=$(part_dev "$1") || return 1
|
|
|
|
|
dir=$(mktemp -d /run/fp6-vendor-blobs.XXXXXX) || fail "mktemp failed"
|
|
|
|
|
# noload succeeds on any ext4 and correctly fails on non-ext4, where
|
|
|
|
|
# plain ro cannot write anyway (erofs); a dirty ext4 is never replayed
|
|
|
|
|
if ! mount -o ro,noload "$dev" "$dir" 2>/dev/null && \
|
|
|
|
|
! mount -o ro "$dev" "$dir" 2>/dev/null; then
|
|
|
|
|
rmdir "$dir" 2>/dev/null
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
MNT=$dir
|
|
|
|
|
MNT_PART=$1
|
|
|
|
|
}
|
|
|
|
|
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
# The listed partitions, space separated, the active slot's first.
|
|
|
|
|
order_parts() { # <partition,...>
|
|
|
|
|
first= rest=
|
|
|
|
|
for p in $(echo "$1" | tr ',' ' '); do
|
|
|
|
|
if [ -n "$ACTIVE_SLOT" ] && [ "${p%_$ACTIVE_SLOT}" != "$p" ]; then
|
|
|
|
|
first="$first $p"
|
|
|
|
|
else
|
|
|
|
|
rest="$rest $p"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
echo "$first $rest"
|
|
|
|
|
}
|
|
|
|
|
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
# Little-endian scalars out of an ELF header. aarch64 is little endian and so
|
|
|
|
|
# is the image, so od's host order is the right one.
|
|
|
|
|
u64() { od -An -tu8 -j "$2" -N 8 "$1" | tr -d ' '; }
|
|
|
|
|
u16() { od -An -tu2 -j "$2" -N 2 "$1" | tr -d ' '; }
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
u8() { od -An -tu1 -j "$2" -N 1 "$1" | tr -d ' '; }
|
|
|
|
|
hex4() { od -An -tx1 -N 4 "$1" | tr -d ' \n'; }
|
|
|
|
|
fsize() { stat -c %s "$1"; }
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
|
|
|
|
|
# Reassemble <dir>/<name>.mdt + .b0N into a flat image at <out>. Mirrors
|
|
|
|
|
# utilities/ta-analysis/reassemble.py in the fp6 bring-up repo, which is where
|
|
|
|
|
# the format was worked out and where the known-good hash comes from.
|
|
|
|
|
# POSIX sh has no locals, so these names are deliberately distinct from
|
|
|
|
|
# extract_mbn's: reassemble() taking rdir= would rewrite its CALLER's copy to
|
|
|
|
|
# the mount path, and the next partition in the retry loop would then be
|
|
|
|
|
# searched at $MNT/$MNT/...
|
|
|
|
|
reassemble() { # <dir> <name> <out>
|
|
|
|
|
mdir=$1 mname=$2 mout=$3
|
|
|
|
|
mdt="$mdir/$mname.mdt"
|
|
|
|
|
[ -f "$mdt" ] || return 1
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
# Structure first, before anything is written: an ELF64 header whose
|
|
|
|
|
# program header table fits in the .mdt, and for every segment with
|
|
|
|
|
# contents a .b0N file of exactly the declared size at a page-aligned
|
|
|
|
|
# offset. This is the whole verification when the manifest pins no hash;
|
|
|
|
|
# the loader's own signature check does the rest.
|
|
|
|
|
[ "$(hex4 "$mdt")" = 7f454c46 ] || { log "$mname.mdt: not an ELF image"; return 1; }
|
|
|
|
|
[ "$(u8 "$mdt" 4)" = 2 ] || { log "$mname.mdt: not ELF64"; return 1; }
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
phoff=$(u64 "$mdt" 32) phentsize=$(u16 "$mdt" 54) phnum=$(u16 "$mdt" 56)
|
|
|
|
|
[ -n "$phoff" ] && [ -n "$phentsize" ] && [ -n "$phnum" ] || return 1
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
[ "$phentsize" -eq 56 ] 2>/dev/null || { log "$mname.mdt: phentsize $phentsize"; return 1; }
|
|
|
|
|
[ "$phnum" -gt 0 ] 2>/dev/null && [ "$phnum" -le 64 ] || { log "$mname.mdt: phnum $phnum"; return 1; }
|
|
|
|
|
[ "$(fsize "$mdt")" -ge $((phoff + phnum * phentsize)) ] || { log "$mname.mdt: shorter than its program header table"; return 1; }
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
|
|
|
|
|
# The image is as long as the furthest segment reaches; everything no
|
|
|
|
|
# segment covers stays zero.
|
|
|
|
|
total=0 i=0
|
|
|
|
|
while [ "$i" -lt "$phnum" ]; do
|
|
|
|
|
o=$((phoff + i * phentsize))
|
|
|
|
|
pfsz=$(u64 "$mdt" $((o + 32)))
|
|
|
|
|
if [ "$pfsz" -gt 0 ]; then
|
|
|
|
|
poff=$(u64 "$mdt" $((o + 8)))
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
seg=$(printf '%s/%s.b%02d' "$mdir" "$mname" "$i")
|
|
|
|
|
[ -f "$seg" ] || { log "$mname: segment $i missing"; return 1; }
|
|
|
|
|
[ "$(fsize "$seg")" -eq "$pfsz" ] || { log "$mname: segment $i is $(fsize "$seg") bytes, header says $pfsz"; return 1; }
|
|
|
|
|
# dd seeks in whole blocks, which is only correct because
|
|
|
|
|
# every p_offset in these images is page aligned. Refuse
|
|
|
|
|
# rather than silently misplace a segment if that changes.
|
|
|
|
|
[ $((poff % 4096)) -eq 0 ] || { log "$mname: segment $i offset $poff is not page aligned"; return 1; }
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
[ $((poff + pfsz)) -gt "$total" ] && total=$((poff + pfsz))
|
|
|
|
|
fi
|
|
|
|
|
i=$((i + 1))
|
|
|
|
|
done
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
[ "$total" -gt 0 ] || { log "$mname: no segment has contents"; return 1; }
|
|
|
|
|
# an order of magnitude above any TA; a garbage p_offset would otherwise
|
|
|
|
|
# make a sparse multi-GiB file that then gets hashed
|
|
|
|
|
[ "$total" -le $((64 * 1024 * 1024)) ] || { log "$mname: image would be $total bytes"; return 1; }
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
: > "$mout" || return 1
|
|
|
|
|
truncate -s "$total" "$mout" || return 1
|
|
|
|
|
|
|
|
|
|
i=0
|
|
|
|
|
while [ "$i" -lt "$phnum" ]; do
|
|
|
|
|
o=$((phoff + i * phentsize))
|
|
|
|
|
pfsz=$(u64 "$mdt" $((o + 32)))
|
|
|
|
|
if [ "$pfsz" -gt 0 ]; then
|
|
|
|
|
poff=$(u64 "$mdt" $((o + 8)))
|
|
|
|
|
seg=$(printf '%s/%s.b%02d' "$mdir" "$mname" "$i")
|
|
|
|
|
dd if="$seg" of="$mout" bs=4096 seek=$((poff / 4096)) \
|
|
|
|
|
conv=notrunc 2>/dev/null || return 1
|
|
|
|
|
fi
|
|
|
|
|
i=$((i + 1))
|
|
|
|
|
done
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
extract_mbn() { # <partition,...> <dir-in-partition> <name> <dest> <sha256|->
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
parts=$1 rdir=$2 rname=$3 dest=$4 want=$5
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
for part in $(order_parts "$parts"); do
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
mount_part "$part" || { log "$part: not mountable, trying next"; continue; }
|
|
|
|
|
[ -f "$MNT/$rdir/$rname.mdt" ] || { log "$part: no $rdir/$rname.mdt, trying next"; continue; }
|
|
|
|
|
tmp="$dest.fp6-extract.$$"
|
|
|
|
|
mkdir -p "${dest%/*}" || fail "cannot create ${dest%/*}"
|
|
|
|
|
if ! reassemble "$MNT/$rdir" "$rname" "$tmp"; then
|
|
|
|
|
rm -f "$tmp"
|
|
|
|
|
log "$part: reassembling $rname failed, trying next"
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
got=$(sha256sum "$tmp" | awk '{print $1}')
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
if [ "$want" != - ] && [ "$got" != "$want" ]; then
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
rm -f "$tmp"
|
|
|
|
|
log "$part:$rdir/$rname sha256 $got != expected, trying next"
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
chmod 644 "$tmp" && mv "$tmp" "$dest" || { rm -f "$tmp"; fail "installing $dest failed"; }
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
log "reassembled $part:$rdir/$rname.{mdt,b0N} -> $dest (sha256 $got)"
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
return 0
|
|
|
|
|
done
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
kept=
|
|
|
|
|
[ -e "$dest" ] && kept=" (the existing file is left in place)"
|
|
|
|
|
if [ "$want" = - ]; then
|
|
|
|
|
fail "no listed partition ($parts) yields a well-formed $rname - $dest NOT installed$kept"
|
|
|
|
|
fi
|
|
|
|
|
fail "no listed partition ($parts) yields $rname with sha256 $want - $dest NOT installed$kept"
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
}
|
|
|
|
|
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
extract() { # <partition,...> <path-in-partition> <dest> <sha256>
|
|
|
|
|
parts=$1 src=$2 dest=$3 want=$4
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
for part in $(order_parts "$parts"); do
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
mount_part "$part" || { log "$part: not mountable, trying next"; continue; }
|
|
|
|
|
[ -f "$MNT/$src" ] || { log "$part: no $src, trying next"; continue; }
|
|
|
|
|
tmp="$dest.fp6-extract.$$"
|
|
|
|
|
mkdir -p "${dest%/*}" || fail "cannot create ${dest%/*}"
|
|
|
|
|
cp "$MNT/$src" "$tmp" || { rm -f "$tmp"; fail "copying $part:$src failed"; }
|
|
|
|
|
got=$(sha256sum "$tmp" | awk '{print $1}')
|
|
|
|
|
if [ "$got" != "$want" ]; then
|
|
|
|
|
rm -f "$tmp"
|
|
|
|
|
log "$part:$src sha256 $got != expected, trying next"
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
chmod 644 "$tmp" && mv "$tmp" "$dest" || { rm -f "$tmp"; fail "installing $dest failed"; }
|
|
|
|
|
log "extracted $part:$src -> $dest"
|
|
|
|
|
return 0
|
|
|
|
|
done
|
|
|
|
|
fail "no listed partition ($parts) yields $src with sha256 $want - $dest NOT installed"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rebind_all() { # <bus/device ...>
|
|
|
|
|
for rb in $1; do
|
|
|
|
|
bus=${rb%%/*} dev=${rb#*/}
|
|
|
|
|
[ -e "/sys/bus/$bus/devices/$dev/driver" ] || continue
|
|
|
|
|
if echo "$dev" > "/sys/bus/$bus/devices/$dev/driver/unbind" 2>/dev/null; then
|
|
|
|
|
log "unbound $bus $dev"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
for rb in $1; do
|
|
|
|
|
bus=${rb%%/*} dev=${rb#*/}
|
|
|
|
|
if [ ! -e "/sys/bus/$bus/devices/$dev" ]; then
|
|
|
|
|
log "rebind: no $dev on bus $bus (yet) - its driver will probe on its own"
|
|
|
|
|
elif echo "$dev" > "/sys/bus/$bus/drivers_probe" 2>/dev/null; then
|
|
|
|
|
log "re-probed $bus $dev"
|
|
|
|
|
else
|
|
|
|
|
log "rebind: drivers_probe of $dev failed (driver not loaded yet?)"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
for arg in "$@"; do
|
|
|
|
|
case "$arg" in
|
|
|
|
|
--if-device) IF_DEVICE=1 ;;
|
|
|
|
|
--refresh) REFRESH=1 ;;
|
|
|
|
|
*) fail "unknown option '$arg'" ;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
if [ -n "$IF_DEVICE" ] && [ ! -b "$SUPER" ]; then
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
log "no stock super partition visible (build chroot?), nothing to do"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
[ -d "$MANIFEST_DIR" ] || exit 0
|
|
|
|
|
|
|
|
|
|
# fast path for every boot after the first: all dests already present
|
|
|
|
|
missing=
|
|
|
|
|
for f in "$MANIFEST_DIR"/*.manifest; do
|
|
|
|
|
[ -e "$f" ] || continue
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
while read -r kind a b c d e; do
|
|
|
|
|
case "$kind" in
|
|
|
|
|
file) [ -e "$c" ] || missing=1 ;;
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
mbn) [ -e "$d" ] && [ -z "$REFRESH" ] || missing=1 ;;
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
esac
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
done < "$f"
|
|
|
|
|
done
|
|
|
|
|
[ -z "$missing" ] && exit 0
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
[ -n "$ACTIVE_SLOT" ] && log "active slot $ACTIVE_SLOT"
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
|
|
|
|
|
for f in "$MANIFEST_DIR"/*.manifest; do
|
|
|
|
|
[ -e "$f" ] || continue
|
|
|
|
|
extracted=
|
|
|
|
|
rebinds=
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
while read -r kind a b c d e; do
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
case "$kind" in
|
|
|
|
|
''|'#'*) ;;
|
|
|
|
|
file)
|
|
|
|
|
[ -n "$d" ] || fail "$f: malformed file line"
|
|
|
|
|
[ -e "$c" ] && continue
|
|
|
|
|
extract "$a" "$b" "$c" "$d" </dev/null
|
|
|
|
|
extracted=1
|
|
|
|
|
;;
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
mbn)
|
|
|
|
|
[ -n "$e" ] || fail "$f: malformed mbn line"
|
fp6-vendor-blobs 1-r3: extract from the active slot, and stop pinning a hash on a signed image
Two field units got no fingerprint sensor from fingerprintd 0.2.3's
manifest: its focal64 line pinned the sha256 of one Android build's
trustlet (16.82.0, the dev phone's), and Fairphone re-signs that trustlet
every release, so the pin matches exactly one of the six builds seen. A
user on 16.100.0 had to edit the manifest by hand; another ended up with
a file QTEE refuses.
The extractor now tries the active slot's partitions first
(androidboot.slot_suffix from the kernel command line): for a signed
image only the running TZ's own slot is guaranteed to load. An mbn line
may give '-' instead of a hash, which means structural verification
only: ELF64 header, every segment present at the size its program header
declares, page-aligned offsets, a sane total. The loader in TZ verifies
the signature and the per-segment hashes itself and refuses a damaged or
foreign image (one flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured),
so the whole-image hash added fragility and no protection. A real sha256
is still honoured, and the sha256 of what was installed is logged either
way.
--refresh re-derives mbn dests even when a file exists, replacing it
only with an image that verifies; consumers call it from
post-install/post-upgrade so a fresh 'apk add' needs no reboot and a
hand-placed or wrongly pinned trustlet is replaced on the next upgrade.
Verified on the dev phone (busybox): malformed inputs are refused with a
reason (missing, truncated or oversize segment; non-ELF, ELF32 or short
mdt; a garbage offset), both slots reassemble to the known-good hash, a
foreign file survives a plain run and is replaced by --refresh, a failed
refresh keeps the old file, pins still work, and the real post-upgrade
path re-derived the installed trustlet with the daemon restarting on it.
Record: fp6 repo journal/blobs/ and journal/fingerprint/, 2026-09-11.
2026-09-11 13:03:22 +02:00
|
|
|
[ -e "$d" ] && [ -z "$REFRESH" ] && continue
|
fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file
The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not
shipped as one file: QTEE images live in the modem partition's image/ as an
ELF header+hashes file plus one payload per program header, and the loader
wants each payload written back at its segment's p_offset. So the existing
file directive cannot reach it.
An mbn directive does, with the same guarantees file has: the sha256 is of
the reassembled image, a mismatch tries the next partition, and an unverified
image is never installed. Reassembly is not a concatenation -- segments are
page aligned but not contiguous, gaps stay zero, and two pairs of focal64's
nine segments share an offset, so they are written in index order and the
later one wins, exactly as the bring-up repo's reassemble.py does.
Verified on the dev phone against the hash QTEE has actually accepted since
August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own
modem_a. The retry path was verified too, with a deliberately wrong first
partition -- which is how the variable clobber got caught: POSIX sh has no
locals, and reassemble() taking rdir= rewrote its caller's copy to the mount
path, so the second partition would have been searched at $MNT/$MNT/...
The fast path needed teaching as well: mbn's dest is the fifth field, and a
first boot would otherwise have exited early and extracted nothing.
Not pushed. The consumer fragment lives in the fingerprintd package.
2026-09-05 03:56:13 +02:00
|
|
|
extract_mbn "$a" "$b" "$c" "$d" "$e" </dev/null
|
|
|
|
|
extracted=1
|
|
|
|
|
;;
|
aports: extract aw88261_acf.bin on-device instead of shipping it
New package fp6-vendor-blobs: a manifest-driven systemd oneshot that
copies proprietary blobs out of the stock Android partitions on first
boot. pmOS installs flash only boot+userdata, so every installed unit
still carries the stock vendor/dsp partitions - the device duplicates a
file it already contains, for its own operation, and nothing proprietary
is distributed by us. Partitions are mounted read-only (ext4 additionally
with noload - not even a journal replay touches the stock data), every
copy is sha256-pinned and a miss fails loudly, and the consuming devices
are unbound and re-probed afterwards so the feature works the same boot.
The rebind is unconditional by design: aw88261 binds on a bare i2c
chip-id probe and requests the ACF only at ASoC card init (~21 s, after
SoundWire enumeration), so a bound device can still be one that lost the
firmware race - and that failed request is never retried.
soc-fairphone-fp6-audio (pkgver 4) stops shipping the blob, installs the
manifest fragment instead, and re-runs the extractor from
post-install/post-upgrade so upgrades restore the file immediately (the
old package version removed it on upgrade). License drops back to plain
BSD-3-Clause - nothing proprietary left in the package.
Verified on the dev phone (fp6 repo journal/blobs/captures/
2026-08-24-first-boot-extractor-verification.txt): first-boot extraction
+ same-boot audio, mid-session post-upgrade recovery, and the everyday
fast-path no-op; committed files byte-identical to the tested deployment.
Assisted-by: Claude:claude-fable-5
2026-08-24 21:00:34 +02:00
|
|
|
rebind)
|
|
|
|
|
[ -n "$b" ] || fail "$f: malformed rebind line"
|
|
|
|
|
rebinds="$rebinds $a/$b"
|
|
|
|
|
;;
|
|
|
|
|
*) fail "$f: unknown directive '$kind'" ;;
|
|
|
|
|
esac
|
|
|
|
|
done < "$f"
|
|
|
|
|
if [ -n "$extracted" ] && [ -n "$rebinds" ]; then
|
|
|
|
|
rebind_all "$rebinds" </dev/null
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
cleanup
|
|
|
|
|
exit 0
|