#!/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 # mbn # rebind # # 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 to , 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). # 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 (.mdt) plus one payload per program header (.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 # later one wins. Same guarantees as file: the sha256 is of the reassembled # image, a mismatch tries the next partition, and an unverified image is # never installed. # rebind: if this fragment's run extracted at least one file, unbind and # re-probe on 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). MANIFEST_DIR=/usr/share/fp6-vendor-blobs/manifest.d SUPER=/dev/disk/by-partlabel/super MNT= MNT_PART= CREATED= TRIED_MAPPING= 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 } # 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 ' '; } # Reassemble /.mdt + .b0N into a flat image at . 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() { # mdir=$1 mname=$2 mout=$3 mdt="$mdir/$mname.mdt" [ -f "$mdt" ] || return 1 phoff=$(u64 "$mdt" 32) phentsize=$(u16 "$mdt" 54) phnum=$(u16 "$mdt" 56) [ -n "$phoff" ] && [ -n "$phentsize" ] && [ -n "$phnum" ] || return 1 [ "$phnum" -gt 0 ] 2>/dev/null || return 1 # 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))) [ $((poff + pfsz)) -gt "$total" ] && total=$((poff + pfsz)) fi i=$((i + 1)) done [ "$total" -gt 0 ] || return 1 : > "$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") [ -f "$seg" ] || { log "$mname: segment $i missing"; 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 } dd if="$seg" of="$mout" bs=4096 seek=$((poff / 4096)) \ conv=notrunc 2>/dev/null || return 1 fi i=$((i + 1)) done return 0 } extract_mbn() { # parts=$1 rdir=$2 rname=$3 dest=$4 want=$5 for part in $(echo "$parts" | tr ',' ' '); do 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}') if [ "$got" != "$want" ]; then 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"; } log "reassembled $part:$rdir/$rname.{mdt,b0N} -> $dest" return 0 done fail "no listed partition ($parts) yields $rname with sha256 $want - $dest NOT installed" } extract() { # parts=$1 src=$2 dest=$3 want=$4 for part in $(echo "$parts" | tr ',' ' '); do 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() { # 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 } if [ "${1:-}" = --if-device ] && [ ! -b "$SUPER" ]; then 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 while read -r kind a b c d e; do case "$kind" in file) [ -e "$c" ] || missing=1 ;; mbn) [ -e "$d" ] || missing=1 ;; esac done < "$f" done [ -z "$missing" ] && exit 0 for f in "$MANIFEST_DIR"/*.manifest; do [ -e "$f" ] || continue extracted= rebinds= while read -r kind a b c d e; do case "$kind" in ''|'#'*) ;; file) [ -n "$d" ] || fail "$f: malformed file line" [ -e "$c" ] && continue extract "$a" "$b" "$c" "$d"