Patch definitions¶
Every patch set in patches/, what it changes, and what is known about it. Base address for
the inflated application image is 0x01000000; addresses below are absolute, and the tools
convert with offset = addr - 0x01000000. The dated record of every car test is in
Hardware verification.
Patch sets in this repository¶
The status column is generated from each file's status field by tools/patch_status.py;
edit the JSON, not this table.
| file | what it changes | status |
|---|---|---|
patches/aux-always-available.json |
IsAUXSRCAvailable() true only — AUX stops greying out |
Confirmed behavioural; no switching |
patches/aux-autoswitch.json |
IsAUXSRCAvailable() true and the inert GetMediaDevice bail-out nop |
Confirmed the first edit on hardware; part of the confirmed boot-to-AUX build (the second edit is inert) |
patches/aux-boot-default.json |
forces C_MGR_SRC::StartUp to restore AUX (position 7) on every boot, ignoring the saved Last_Source |
Confirmed with aux-boot-restore boots to AUX with aux-boot-restore (2026-09-28, NAV); alone it still boots to FM |
patches/aux-boot-restore.json |
lets AUX's PrOnly request reach the boot restore, and forces the restored priority to AUX's 20; pair with aux-boot-default |
Confirmed with aux-boot-default booted to AUX three times (2026-09-28, NAV); the spy capture shows AUX restored and acknowledged at request time |
patches/aux-no-idle-mute.json |
drops the no-signal mute on AUX: RadioMuteManager stops treating C_MODULE_AUDIO+0x168 as a mute source |
Never flashed the mute decision verified under emulation on all three builds; no car has run it |
patches/aux-signal-switch.json |
the AUX handler reads the signal instead of the setting, and the media dispatch sends the signal event (0xcc) to it; pair with aux-boot-restore and aux-sticky |
Never flashed both functions verified under emulation; not being pursued |
patches/aux-sticky.json |
removes the bail-out and turns "AUX setting switched off" into a no-op | Never flashed control flow verified under emulation; not being pursued |
patches/diagnostic-logging.json |
redirects the logging stub to the real logger | Not for driving diagnostic build; needs the mask patch too |
patches/diagnostic-logmask.json |
forces the global trace mask — necessary but not sufficient, see below | Not for driving diagnostic build |
patches/diagnostic-logsink.json |
points the stubbed Log_msg sink at VxWorks logMsg; where its output surfaces is issue #94 |
Not for driving diagnostic build; never flashed |
patches/spy-dump-userdata-partition.json |
meant to make SPYSTORE copy the whole USER_DATA partition root, in place of the calibration and regen files; no copy reached the stick |
Does not work flashed 2026-09-27 and 2026-09-28; SPYSTORE put no USER_DATA tree on the stick (observed 2026-09-28) - use spy-dump-userdata |
patches/spy-dump-userdata.json |
makes SPYSTORE also copy /USER_DATA/user_data out to the stick |
Confirmed on hardware (2026-09-14, NAV) |
What works on hardware (NAV, 5.43.A.R2): a re-sealed package flashes; AUX stays available
with no signal (IsAUXSRCAvailable()); the unit boots to AUX with
builds/aux-boot.json; and SPYSTORE can
back up /USER_DATA/user_data. The unit does not switch to AUX by itself when audio starts:
no shipped patch does that, and the handler most AUX patches touch reacts to the saved AUX
input setting, not to a signal (see The AUX signal path).
The application image¶
Every patch here edits <module>/AppBin/f_BigQuick.bin:
0x0000..0x0800 header: 64 x 32-byte entries, of which only 0 and 1 are used
0x0800 0x08 (compression marker)
0x0801.. a zlib stream -> the application image
0xEE pad to the 2 KiB block the header names
(blocks+1)*2048 0x08 + a second zlib stream -> a second PowerPC image
Inflating the first stream yields the application image (about 32 MB for AUDIO_BT, about 40 MB
for NAV), a raw PowerPC image loaded at 0x01000000. It is not encrypted. The header carries
the inflated size at +0x04 of entry 0 and, at +0x14, the stored span of that
segment — its 0x08 marker, the compressed bytes and two trailing bytes — which is the closest
thing to a compressed size the format has. The symbol map SPYSTORE copies off the
unit (abs_symbols_base.txt.gz) lines up exactly with the inflated image — every function
symbol lands on a PowerPC prologue (stwu r1,-N(r1) ; mflr r0) — which is what makes
symbol-level patching possible. tools/unpack.py inflates it; patch_smeg.py re-packs it and
walks the checksum cascade (f_BigQuick.bin → .inf → smeg.inf → <module>_ctrl.bin →
ctrl.bin, see Boot and update chain).
The header is a table of 32-byte entries: entry 0's first word is the constant 0x00010004,
and a later entry's first word is its segment's offset in 2 KiB blocks (file offset =
(blocks + 1) × 2048), with +0x04 its inflated size, +0x14 its stored span and 0xdeadbeef
at +0x1C. (Read from bousqi/SMEG_PLUS's smeg_reverse.txt, then executed against the NAV,
AUDIO_BT and AUDIO_BT_256 images — tests/test_firmware_nav.py.) Two things the real
images settled against the original description: blocks is not 0, so a later entry points
at a second segment — a second 0x08 marker and zlib stream — rather than at the
application's own stream at 0x801; and the entries past the first two are eight 0xdeadbeef
words, unused slots rather than segments. See Sources and prior art.
Addresses are per firmware version, not only per build¶
Every address on this page was read out of the SMEG_5.43.A.R2 NAV image. AGENTS.md
already warns that addresses differ between the AUDIO_BT, AUDIO_BT_256 and NAV builds.
They also differ between firmware versions, and by more than a few bytes.
Comparing the NAV application images from SMEG_5.42.B.R4 (Nov 2016) and SMEG_5.43.A.R2
(Sep 2017):
| what | 5.43.A.R2 |
5.42.B.R4 |
shift |
|---|---|---|---|
IsAUXSRCAvailable() |
0x02247858 |
0x022477c0 |
−152 |
C_MGR_SRC setter (Last_Source) |
0x016977d0 |
0x01697738 |
−152 |
C_MGR_SRC SPY dump |
0x0169a2e4 |
0x0169a24c |
−152 |
C_MGR_SRC class strings |
0x0300a6a4 |
0x0300a624 |
−152 |
Time_Zone string |
0x02fcc5f4 |
0x02fcc574 |
−152 |
The two images are the same code displaced by a constant 152 bytes. That is why a naive byte-for-byte comparison calls ~80% of the image different: it is mostly the shift, not new code. Not everything is displacement though — the AUX handler region does not match verbatim at any offset, so there are real changes there as well.
If your unit is not on 5.43.A.R2
The addresses in patches/*.json will be wrong for it, and writing to them would corrupt
the image.
The expect bytes are not a sufficient guard. Two entries in this repository match at
the same address on the 5.42.B.R4 NAV image — diagnostic-logging and
diagnostic-logsink, both at 0x010346d0 — because a short instruction sequence recurs
across versions. On that image the expect check passes and the edit lands 152 bytes off.
So every variant declares the version it was derived from:
patch_smeg.py refuses unless the inflated image carries that token, which the vendor's own
build path supplies (E:/ccm_wa71/04_HMI_DEV-5.43.A.R2/...). It reports the version it did
find, so a mismatch explains itself:
NAV: this image is not 5.43.A.R2 - refusing to patch.
Addresses are per firmware version, and the expect-byte check is not a
reliable substitute: short instruction sequences recur across versions.
Build tokens found in this image: 5.42.B.R4.1
Porting is mostly mechanical for unchanged code (subtract 152 here), but the AUX handler changed, so a 5.42 port needs that address re-derived from a 5.42 symbol table rather than shifted. Addresses must be re-derived per firmware version, not copied.
tools/symdiff.py — find out which of that is mechanical¶
The tool works out the shift and separates the two cases that matter — moved-and-unchanged, where the address follows from the displacement alone, from moved-and-changed, where it does not:
python3 tools/symdiff.py --a old.bin old_symbols.txt --b new.bin new_symbols.txt
python3 tools/symdiff.py --a old.bin old_symbols.txt --b new.bin new_symbols.txt \
--patch-addr 0x02247858
shift +152 (agreed by 3 of 3 moved symbols)
identical 1184 a patch here moves by the shift alone
changed 61 address carries over, bytes need re-deriving
It reads a symbol map in the same <addr> <type> <name> form ppcdis/xref do, because a
map one tool accepts and another rejects would be a trap.
A derived address is a candidate, not a patch. The tool prints that itself, and it is the
rule from AGENTS.md: an address without the expect bytes to back it is not safe to apply,
and patch_smeg.py refuses one that does not match anyway. Use this to know where to look,
then re-derive the expect bytes from the target image.
Which build is this? tools/fingerprint.py¶
The version check above covers which firmware. The build — AUDIO_BT, AUDIO_BT_256,
NAV — is easy to get wrong by hand: AUDIO_BT and AUDIO_BT_256 declare identical patch
addresses and identical expect bytes, so nothing in the patch data separates them.
tools/fingerprint.py reads the image and answers from the bytes:
image 39863376 bytes, already inflated
build tokens 5.43.A.R2
aux-autoswitch/AUDIO_BT 0/2 probes firmware=5.43.A.R2
aux-autoswitch/AUDIO_BT_256 0/2 probes firmware=5.43.A.R2
aux-autoswitch/NAV 2/2 probes firmware=5.43.A.R2 <- match
...
verdict: NAV
It exits non-zero when it cannot name a single build. Three outcomes, and only one is success:
| verdict | meaning |
|---|---|
| a build name | exactly one build's probes all match — exit 0 |
AMBIGUOUS |
several builds match, so the probes cannot choose between them — exit 1 |
UNKNOWN |
nothing matches; the build tokens actually present are printed — exit 1 |
It refuses rather than guesses, by design. On the firmware in this repository the
AUDIO_BT and AUDIO_BT_256 variants are indistinguishable from the recorded addresses, so a
tool that picked one would make the same mistake a person would, silently. Verified by hand
against the 5.43.A.R2 NAV image, where it identifies NAV and rejects every AUDIO_BT
variant.
patch_smeg.py calls the same identification before patching. It stays quiet when an image
matches nothing, because the firmware-token check and the per-patch expect check already
report that precisely. What it adds is the wrong-build case, which otherwise surfaces as a
confusing byte mismatch:
NAV: this image is not the NAV build - refusing to patch.
It matches: AUDIO_BT. Addresses are per build, so patching it with these addresses
would write to the wrong locations. Run tools/fingerprint.py for a verdict.
The probes are the recorded expect bytes themselves, so a build only fingerprints as well as
the patch data for it is accurate. A build with no patch set here is still named from the
vendor build path inside its image (build_tokens in fingerprint.py's output), but no patch
set can be selected for it: its addresses have to be derived first (see tools/symdiff.py
above).
AUX availability: aux-always-available, aux-autoswitch¶
C_HMI_AUDIO_APP_BASE::IsAUXSRCAvailable() — force available¶
Replaces the function prologue with li r3,1 ; blr, so AUX is reported available
regardless of vehicle config or signal. The AUX source stays selectable, no longer greys out
when there is no signal, and is always in the SRC cycle (confirmed on hardware). It does not
make the unit switch by itself.
| build | address | original | patched |
|---|---|---|---|
AUDIO_BT, AUDIO_BT_256 |
0x02247718 |
94 21 ff a0 (stwu r1,-0x60(r1)) |
38 60 00 01 4e 80 00 20 (li r3,1 ; blr) |
NAV |
0x02247858 |
94 21 ff a0 |
38 60 00 01 4e 80 00 20 |
aux-always-available is this edit alone. aux-autoswitch is this edit plus the one below.
HandleAudioAuxInputStatusChnged() +0x10c — the inert second edit of aux-autoswitch¶
At handler + 0x10c the function returns early when GetMediaDevice(AUX) fails, before it
reaches ActivateSource(). This edit replaces that conditional branch with nop.
| build | handler | branch address | original | patched |
|---|---|---|---|---|
AUDIO_BT, AUDIO_BT_256 |
0x023031dc |
0x023032e8 |
41 9e 01 4c (beq cr7,+0x14c) |
60 00 00 00 (nop) |
NAV |
0x0230331c |
0x02303428 |
41 9e 01 4c |
60 00 00 00 (nop) |
It has no effect (executed). The branch is never taken: the AUX media device is
registered unconditionally at start-up, so GetMediaDevice(AUX) succeeds and the beq falls
through with or without the patch. Forcing the failure case does not help either —
GetMediaDevice writes nothing to its out-param when it fails, so the source-manager guard at
0x02303468 returns one call later. The runs are in Emulating the firmware.
The edit is kept in aux-autoswitch because that is the exact set flashed in the confirmed
boot-to-AUX build. The handler itself runs when the saved AUX input setting changes
(Auxiliary_Status, message 0xcb), not when a signal appears (read); see
The AUX chain.
Boot to AUX: aux-boot-default + aux-boot-restore¶
Confirmed on hardware (NAV, 2026-09-28): with both sets, plus aux-autoswitch, the unit
booted to AUX three times, and the spy capture of the last boot shows why
(executed; see Hardware verification). Each set alone still boots to FM,
as does aux-boot-restore without its InitApp edit (executed on the car).
builds/aux-boot.json
is the minimal build: exactly those three sets.
How the unit picks its boot source (read, and AddRequest executed under emulation):
C_MGR_SRC::StartUprestores the savedLast_Source(a scheduler position) andLast_Source_Priorityintothis+0xb4andthis+0xac, and starts a 7.5 s init timer.- As each source app starts, it requests its source.
C_MGR_SRC::AddRequestadds a request's (Sched_Pos, priority) to theScheduledInittable and compares it with the restored pair — only if the request'sPrOnlybyte (+0x28) is clear. - A match cancels the timer and selects that source. If nothing matches in 7.5 s, the timer falls back to position 1, the tuner.
On stock firmware AUX loses on two counts: its boot request carries PrOnly true, so it never
enters the table, and the saved Last_Source is whatever the unit last played. The two sets fix
one each.
aux-boot-default — make the restore target AUX¶
Last_Source is not a fixed preference. C_MGR_SRC::ImmediateSourceSave (0x01695d68) writes
the active source back to it whenever the source changes, so a settings-database seed lasts only
until the next source change. This patch changes the restore instead of the saved value.
Inside C_MGR_SRC::StartUp the restore reads Last_Source and stores it, unvalidated, into the
field the scheduler later matches on:
0169948c lwz r9, 8(r1) ; r9 = saved Last_Source
01699490 stw r9, 0xb4(r31) ; this+0xb4 (matched against each node's Sched_Pos)
01699494 stw r9, 0x4cd0(r25) ; global 0x035e4cd0 (mirror)
0169949c stw r9, 0xe4(r31)
Replacing the load with a constant pins the restore to position 7 (POS_AUX):
| build | address | original | patched |
|---|---|---|---|
NAV |
0x0169948c |
81 21 00 08 (lwz r9,8(r1)) |
39 20 00 07 (li r9,7) |
AUDIO_BT, AUDIO_BT_256 |
0x01699334 |
81 21 00 08 (lwz r9,8(r1)) |
39 20 00 07 (li r9,7) |
Emulated on the NAV image, with the saved value set to 1 (radio) (executed):
| build | this+0xb4 |
global 0x035e4cd0 |
this+0xe4 |
|---|---|---|---|
| stock | 1 | 1 | 1 |
| patched | 7 | 7 | 7 |
The Last_Source line in the C_MGR_SRC spy trace is written just before this store
(WriteMgrSrcSpy at 0x01699488), so it shows the saved value, not the patched one
(read). The AUDIO_BT variants are the same instruction at the same offset in StartUp,
located with each build's own symbol map (read); they have never been flashed.
aux-boot-restore — let AUX's request take part in the restore¶
PrOnly is the bool argument of C_HMI_SrcMgntBase::ActivateSource. Two places pass true
for AUX: C_HMI_MEDIA_APP_BASE::InitApp, whose request is the one the boot restore sees, and
HandleAudioAuxInputStatusChnged, which re-requests AUX when the AUX input setting changes.
This set makes both pass false, and forces the restored priority to AUX's 20 so that AUX's
pair (7, 20) matches:
| build | address | original | patched |
|---|---|---|---|
NAV |
0x022c0678 |
38 80 00 01 (li r4,1: the boot-time ActivateSource(aux, true) in InitApp) |
38 80 00 00 (li r4,0) |
NAV |
0x02303474 |
38 80 00 01 (li r4,1: ActivateSource(aux, true) in HandleAudioAuxInputStatusChnged) |
38 80 00 00 (li r4,0) |
NAV |
0x01699444 |
80 01 00 08 (lwz r0,8(r1), the saved Last_Source_Priority) |
38 00 00 14 (li r0,20) |
AUDIO_BT, AUDIO_BT_256 |
0x022c0538 |
38 80 00 01 (InitApp) |
38 80 00 00 |
AUDIO_BT, AUDIO_BT_256 |
0x02303334 |
38 80 00 01 (the handler) |
38 80 00 00 |
AUDIO_BT, AUDIO_BT_256 |
0x016992ec |
80 01 00 08 (StartUp) |
38 00 00 14 |
The InitApp site is the only one of InitApp's six activations that passes true, so no
other source changes. Under emulation, AddRequest with PrOnly clear fills ScheduledInit
with (7, 20), sets the restore flag (+0x3c0) and cancels the init timer, and does none of that
with PrOnly set (executed; tests/test_firmware_nav.py).
The AUDIO_BT addresses are not the NAV ones moved by the image-wide shift (−344): the media
app's functions sit at −320, so each site is the same offset in the same function, found with
the build's own symbol map. InitApp has the same six activations with only the fifth passing
true. On both AUDIO_BT images the same AddRequest runs give the same results, and the
patched handler passes PrOnly 0 where stock passes 1 (executed;
tests/test_firmware_audio_bt.py). Neither variant has been flashed.
On the car, the capture of a boot with both sets shows AUX's request with PrOnly false,
POS_AUX (7, 20) in ScheduledInit, and AUX acknowledged in the same millisecond it asked,
while the saved Last_Source was 1 — so aux-boot-default's override made the match
(executed).
After boot, a second AUX request is forced to the front (ForceSchedulerPosition). The handler
sends one only when the AUX input setting goes from zero to non-zero, so this does not switch
to AUX when audio starts. One more AUX activation path exists outside this set:
HandleMediaStateReady, with a computed PrOnly at 0x02306a18. Whether it ever runs for AUX
is not known; boot to AUX works without touching it. See
How HMI apps request sources.
The AUX idle mute: aux-no-idle-mute¶
Never flashed — one edit, verified under emulation on all three builds and never on a car. A CarPlay/Android-Auto piggyback plays into the AUX input, and the unit mutes AUX while it sees no signal there, so AUX is silent until something plays. This set stops that idle mute, and only that: it does not switch to AUX when a signal appears, which the project is not pursuing (The AUX signal path).
What the mute actually is¶
C_MODULE_AUDIO+0x168 is the AUX no-signal mute, and the vendor's own log text names it: the
GetMuteParameters dump prints it as AUXSign_mute (0x02f94da8), and RadioMuteManager's
own line prints it as aux_mute (0x02f957e0). (read)
Three functions arm it from Radio::Get_AUX_signal_status (0x0131ae40); both constructors
zero it. All four are read:
| function | address | when |
|---|---|---|
Elab_AUDIO_AUX_SIGNAL_STATUS_CHANGED |
0x013ce870 |
radio event 0x3d, the AUX signal change: sets it to 1 while the current source is AUX and the signal is absent, clears it when the signal returns |
ElabRADIO_STARTED |
0x013d565c |
radio start, if the current source is AUX |
Cmd_change_source |
0x013b8478 |
a source change to AUX (type 5) |
It has one functional reader. Scanning every instruction in the image whose displacement is
0x168, and grouping by the function each belongs to, leaves C_MODULE_AUDIO::RadioMuteManager
(0x013b3c5c) as the only one that turns the flag into anything: there it is one term in the
DSP mute decision,
013b3df4 lbz r0, 0x168(r30) ; aux_mute
013b3df8 cmpwi cr7, r0, 0
013b3dfc bne cr7, +0x6c ; -> li r31,1 : muted
alongside every other term that forces the same muted result — the user mute (+0x15d with
+0x1ac), the system mute (+0x15c), the timed mute (+0x169) and a null or zero source
volume. Every other reader is spy or log telemetry — GetMuteParameters
(0x013b1a90), CmdSystemMute (0x013b4098), Set_amplifier_mute (0x013b272c) and
generateAudioSpy (0x013d05ac) — confirmed from the format strings each passes to
spyRegEv/Log_msg. (read)
So this is a software flag feeding one decision, not a DSP-side gate we cannot reach:
RadioMuteManager ends in CmdMute(muted) to the radio. Dropping the flag's one consumer
drops the mute.
The edit¶
| build | address | original | patched |
|---|---|---|---|
NAV |
0x013b3dfc |
40 be fe cc (bne cr7,+0x6c) |
60 00 00 00 (nop) |
AUDIO_BT, AUDIO_BT_256 |
0x013b3ca4 |
40 be fe cc |
60 00 00 00 |
The condition is kept and only the test is replaced, so no other mute term changes. +0x168 is
only ever set while the current source is AUX, so nothing else is affected; the flag is still
computed and still appears in the spy, it just no longer mutes.
The AUDIO_BT address is the same instruction at the same offset (+0x1a0) inside the same
function, which sits at 0x013b3b04 in that build (NAV 0x013b3c5c, a shift of −0x158). Both
AUDIO_BT builds ship byte-identical symbol maps and identical addresses, which is why
tools/fingerprint.py reports them AMBIGUOUS rather than choosing.
Emulated¶
tools/ppcemu.py on the stock NAV, AUDIO_BT and AUDIO_BT_256 images; the same runs are in
tests/test_firmware_nav.py and tests/test_firmware_audio_bt.py. Every callee is stubbed —
Radio::Get_AUX_signal_status (writing the signal under test), CmdMute (recording its
muted argument), Call_action, RadioMuteManager where the handler is the subject, the
watchdog helpers and AmplifierMuteManager. All three builds give the same table:
+0x168 |
CmdMute(muted) stock |
CmdMute(muted) patched |
|---|---|---|
| 1 | 1 | 0 |
| 0 | 0 | 0 |
Each of the other terms still mutes with the patch applied — user mute, system mute, timed
mute and a source volume of zero all reach CmdMute(muted=1) on the patched image, which is
what says the edit removed one term and not the decision. (executed)
Chained, which is the claim rather than the mechanism: Elab_AUDIO_AUX_SIGNAL_STATUS_CHANGED
run first with no signal on AUX leaves +0x168 = 1 and calls RadioMuteManager; the same
object then reaches CmdMute(muted=1) stock and CmdMute(muted=0) patched. The other arms are
executed too: with the signal present and the flag clear the handler changes nothing and does
not call RadioMuteManager at all, and with the signal present and the flag set it clears the
flag before re-running the decision. (executed)
What is not verified¶
- The car. No build carrying this set has been flashed.
statusisnever-flashedand no claim is made that it works. - The DSP. The DIRANA2's detector is armed by
SetCfgAuxPrimary; whether it can gate the AUX audio independently of this flag was not read. This set removes the software mute path only. - That the input is then audible. Emulation proves which command the unit sends, not what comes out of the speakers.
What a car test would have to show¶
builds/aux-boot-no-idle-mute.jsonis boot to AUX plus this set: the unit must still boot to AUX, so the change did not disturb the boot path (builds/aux-boot.jsonis the confirmed baseline).- On AUX with the source connected and nothing playing, the input is currently silent. With this set it should not be gated: the input's own noise floor should be audible, and a track should start from its first moment rather than after a delay.
- Pause and resume: AUX must not drop out between the two, and the unit must not fall back to the tuner.
- The other mutes must still work: the audio menu's system mute, user mute, and a source volume of zero must each still silence the unit.
Not pursued: aux-sticky, aux-signal-switch¶
Both are candidates, verified under emulation only and never flashed. The owner is happy with boot to AUX and is not pursuing a switch to AUX when audio starts.
aux-sticky¶
Two edits in HandleAudioAuxInputStatusChnged():
| offset | original | patched | effect |
|---|---|---|---|
+0x10c (AUDIO_BT 0x023032e8, NAV 0x02303428) |
beq |
nop |
the inert edit above, kept so the set is a superset of aux-autoswitch |
+0x118 (AUDIO_BT 0x023032f4, NAV 0x02303434) |
beq cr7,+0x58 |
beq cr7,+0x140 |
when the AUX input setting is zero, branch to the shared return path instead of the release branch |
The second edit means that once AUX has been activated it stays selected until the user
changes source. It follows the saved AUX input setting, so on its own it does not address
intermittent audio on the AUX input (inferred). The condition is kept and only the target
changes; an unconditional b +0x140 would make the activate path below unreachable.
tests/test_patch_definitions.py refuses any edit that turns a conditional branch into an
unconditional one unless why says so.
Emulated on all three images (NAV, AUDIO_BT, AUDIO_BT_256), with the call targets decoded
from each build's own lis/addi pairs (executed):
| bytes at the branch | setting becomes non-zero | setting becomes zero |
|---|---|---|
419e0058 stock |
activates | releases |
419e0140 patched |
activates | does not release |
The handler lives at 0x0230331c on NAV and 0x023031dc on both AUDIO_BT variants, and the
offsets within it (+0x10c, +0x118, +0x258) are the same in all three.
aux-signal-switch — switch to AUX when its signal appears¶
Stock firmware has no switch-on-signal path. The AUX handler runs on the setting event
(0xcb) and tests the setting, and the media app drops the signal event (0xcc)
(read). This set changes both:
| build | edits | what |
|---|---|---|
NAV |
0x02303378, 0x0230338c, 0x02303398, 0x0230342c |
HandleAudioAuxInputStatusChnged calls Get_AUX_signal_status instead of Get_aux_status, and reads its bool result |
NAV |
0x02309628–0x02309648 (8 words) |
HandleDBUSMessage gains a 0xcc case going to the same handler; every other id is routed as before |
The exact bytes are in The AUX signal path. Under tools/ppcemu.py on the
NAV image (executed): the patched handler activates AUX when the signal appears and releases
it when the signal is lost; with aux-boot-restore the activation passes PrOnly 0; with
aux-sticky the release is suppressed; the dispatch sends 0xcc to the handler and every
other id where stock sends it. Whether the unit measures AUX while another source plays is
not known; if it does not, this cannot switch away from FM.
builds/aux-signal-switch.json is the car build.
SPYSTORE backups: spy-dump-userdata, spy-dump-userdata-partition¶
spy-dump-userdata — SPYSTORE also backs up /USER_DATA¶
Confirmed on hardware (NAV, 5.43.A.R2, 2026-09-14): with this patch flashed, SPYSTORE
with a stick inserted produced a dump (SPY/02_.../) containing the full
/USER_DATA/user_data/ tree — sqlite/ (14 databases with their .inf CRC sidecars),
Audio/ (Tuner.dat/Radio.dat presets), and Nav/, TTS/, T2BF/. The files are the
live copies: nav_dest.sqlite is a valid SQLite file, and up_common.sqlite/up_user.sqlite
are stored gzip'd (1f8b …), which is how the unit keeps them on disk (the boot log's
gzUnixRead).
connectivity.sqlite is not captured: it is not a file under
/USER_DATA/user_data/sqlite/ — the boot log shows it is imported from the system partition
(connectivity imported from system) — so paired phones are out of scope of this backup.
Navigation destinations (nav_dest.sqlite), radio presets (Audio/*.dat) and general
settings (up_common) are captured.
C_BCM_SPY::CallBackCopy (NAV 0x01273734) is the routine SPYSTORE runs to copy the spy
directory out to a stick. It is a sequence of Get<X>Dir source getters each followed by
C_FS_STORAGE_CTRL_IO::Xcopy(source, dest) into a timestamped folder on the stick
(<stick>/SPY/<timestamp>); see Cheatcodes. None of those sources is the
live settings partition, so a stock collect never captures the user's databases.
The firmware already exports the primitive that fixes this:
C_FS_STORAGE_CTRL_PATH::GetUserDataDir (0x0105ae44) points an entity at
/USER_DATA/user_data/. So the added copy is one more block of exactly the existing shape:
GetUserDataDir(r29) ; r29 = /USER_DATA/user_data/ (source)
Xcopy(r29, r31) ; r31 = <stick>/SPY/<timestamp> (dest)
CallBackCopy has no spare space, and there is no usable code cave inside .text (see
the note below). So the patch is cave-free: it overwrites the last of the two
calibration-copy blocks — the *regen* one — in place. That 48-byte block is more than the
nine instructions the replacement needs.
| build | address | original (*regen* copy) |
patched |
|---|---|---|---|
NAV |
0x01273a1c |
GetCalibrationDataDir ; AddName "*regen*" ; Xcopy (12 instr) |
GetUserDataDir(r29) ; Xcopy(r29,r31) ; nop×3 |
lis r9, 0x106 # 3d200106
addi r9, r9, -0x51bc # 3929ae44 -> r9 = GetUserDataDir (0x0105ae44)
mtctr r9 # 7d2903a6
mr r3, r29 # 7fa3eb78 -> source entity
bctrl # 4e800421 -> GetUserDataDir(r29)
mr r3, r29 # 7fa3eb78 -> source
mr r4, r31 # 7fe4fb78 -> dest (stick SPY/<timestamp>)
mtctr r26 # 7f4903a6 -> r26 still holds Xcopy (0x010554f4)
bctrl # 4e800421 -> Xcopy(r29, r31)
nop ; nop ; nop # 60000000 ×3
Why this is safe to write in place:
r29(the source entity) andr31(the destination) are callee-saved registers and are live here — the original block uses both at this exact point.r26already holdsXcopy(0x010554f4): the very block being replaced doesmtctr r26for its ownXcopy, so the value is guaranteed valid, and the replacement does not reload it.- The trailing
nops keepXcopy's return inr3intact for thecmpwi r3,-1at0x01273a4cthat follows, so the function's existing success/error handling is unchanged.
Verified by round-tripping the bytes through capstone and by tests/test_spy_dump_userdata.py,
which decodes the lis/addi pair to confirm the callee is GetUserDataDir and applies the
shipped definition end-to-end through patch_smeg.py.
Trade-offs
- The dump loses the
*regen*calibration files in exchange for the/USER_DATAbackup. That is the cost of staying cave-free. - NAV only.
AUDIO_BT/AUDIO_BT_256have a differentCallBackCopyaddress; derive it from each build's own image before adding those variants. - This reads
/USER_DATAbut does not write it, so it cannot damage the user partition — unlike aUSER_DATApayload build.
Why a trampoline is out of reach, and what would change that
Every run of 32 bytes or more of nops/zeros in the image lies in .rodata —
CMMStrBufEncodedUTF8's table, sqlite3_version, utf8proc_sequences and friends — so it
is live data, not padding. Text ends at 0x02def4c0.
A trampoline would therefore have to live past the end of the image, and that is
mechanically expressible rather than impossible: the container header carries the
inflated size at offset 0x04 (0x02604450 on this NAV image, verified) and no
compressed size, because the zlib stream is self-delimiting. The image can be grown and
that field updated. Whether the loader maps the appended region executable is
not known, so a trampoline is theoretical.
spy-dump-userdata-partition — the whole partition (does not work)¶
Flashed, and the copy did not land (observed). This set was in the 2026-09-27 and
2026-09-28 car builds and SPYSTORE was run after each. The stick's SPY/<stamp>/ folder from
the 2026-09-28 run, checked in full, holds the normal collect and no USER_DATA tree; the
calibration *.log and *regen* files are gone, as the edit removes them. Why Xcopy of the
device root copied nothing is not known. Use spy-dump-userdata for a settings backup.
The idea: spy-dump-userdata captures /USER_DATA/user_data, and its siblings are
separate directories on the same storage device:
/user_data <- what spy-dump-userdata gets: settings, nav destinations, presets
/address_book /internet_user /welcome_screen
/picture_cache /catalog /TurboBoot
The path-fragment table at 0x02f08418 shows each C_FS_STORAGE_CTRL_PATH::Get*Dir is only
SetDevice(<device>) + SetPartition(n) + AddName("<fragment>") (read), so device + partition
with no name should be that device's root. GetUserDataDir is USER_DATA, partition 4,
/user_data; GetAddressBookDir is the same device and partition with /address_book.
So this patch rebuilds the source entity as the root — fresh constructor,
C_FS_STORAGE_DEVICE_USER_DATA::Instance(), SetDevice, SetPartition(4), deliberately no
AddName — and copies that:
mr r3, r29 # source entity
mtctr r23 # r23 = C_FS_STORAGE_ENTITY::C1 (0x01068d24), already loaded
bctrl # fresh entity: no names, so it is the device root
lis r9, 0x0106
addi r9, r9, 0x702c # -> USER_DATA::Instance() (0x0106702c)
mtctr r9
bctrl
mr r4, r3
mr r3, r29
lis r9, 0x0107
addi r9, r9, -0x773c # -> SetDevice (0x010688c4)
mtctr r9
bctrl
lis r9, 0x0107
addi r9, r9, -0x76d4 # -> SetPartition (0x0106892c)
mtctr r9
li r4, 4
mr r3, r29
bctrl
mr r3, r29
mr r4, r31
mtctr r26 # r26 still holds Xcopy (0x010554f4)
bctrl
nop ×7 # pads the reclaimed 120 bytes
| build | address | replace | patched |
|---|---|---|---|
NAV |
0x012739dc |
120 bytes — the calibration *.log block and the *regen* block, ending just before the SYSTOOL_PlayBeep_Spy setup at 0x01273a54 |
the routine above |
Its call sequence was executed under tools/ppcemu.py, with every callee stubbed
(executed):
| call sequence through the copy blocks | |
|---|---|
| stock | … GetApplicationDir, Xcopy, GetCalibrationDataDir, Xcopy, GetCalibrationDataDir, Xcopy |
| patched | … GetApplicationDir, Xcopy, USER_DATA::Instance, SetDevice, SetPartition, Xcopy |
That proves the control flow and the call targets, not what Xcopy does with a device root on
the unit — which, on the evidence above, is nothing that reaches the stick.
It overwrites the same 0x01273a1c block as spy-dump-userdata, so the two cannot both be
applied: the expect check refuses the second. NAV only.
Diagnostic logging: diagnostic-logmask, diagnostic-logsink, diagnostic-logging¶
Not for driving
These are diagnostic builds. With logging enabled the firmware formats a message on every
path it takes. Do not drive on them, and flash a normal build afterwards. Never flash
diagnostic-logging together with diagnostic-logmask (below).
The application's logging has no output path in this build (read and executed):
Log_msg(0x02742558) returns at a gate unless(GetLogMask() & level) != 0.GetLogMask(0x02742530) reads one global at0x036d42a8, which is past the end of the image (0x03604450) — BSS, so zero at boot. Exactly one instruction writes it, reachable only through tenSetTracewrappers that are vtable entries, so nothing in the ordinary start-up path turns logging on.- Past the gate,
Log_msgmakes one more call: to0x010346d0, the sink, which the vendor shipped asli r3,0 ; blr. The ~6700 call sites that calldummyLogMsgdirectly end at the same no-op, becausedummyLogMsgis0x010346d0.
So a working diagnostic needs both the mask forced and the sink pointed at something that
writes: builds/diagnostic-logging.json is diagnostic-logmask + diagnostic-logsink. Where
logMsg output surfaces on the unit — serial, telnet, a file, or nowhere reachable — is
not known (issue #94); none of these three has been flashed.
The reason it would be worth having: HandleAudioAuxInputStatusChnged logs its own name at
level 1 on its shared return path, which every one of its exits and its success path reach
(executed):
0230331c HandleAudioAuxInputStatusChnged()
…
02303574 li r3, 1 ; level
0230357c addi r4, r9, -0x2948 ; "HandleAudioAuxInputStatusChnged() -\n"
02303590 bctrl Log_msg
With logging readable, that line would show whether the handler ran on a given event.
diagnostic-logmask — force the trace mask¶
| build | address | original | patched |
|---|---|---|---|
NAV |
0x02742530 |
94 21 ff f0 93 e1 00 0c (prologue) |
38 60 ff ff 4e 80 00 20 (li r3,-1 ; blr) |
GetLogMask returns all ones, so the ~5900 call sites that go through Log_msg run to
completion instead of returning at the gate. Same idiom as the IsAUXSRCAvailable() patch —
overwrite a prologue with a constant return, no code cave, trivially reversible. Emulating
Log_msg: with the mask at 0 it bails after 47 instructions; forced, it runs 271 and calls its
sink (executed). On its own it produces no output, because the sink is the no-op.
diagnostic-logsink — point the sink at VxWorks logMsg¶
The caller passes a format string in r3 and up to six arguments in r4–r9, which is
exactly VxWorks logMsg(fmt, a1…a6).
logMsg is at 0x00484a94, and that is now read off the symbol table rather than inferred.
(executed, tests/test_firmware_bsp.py) BSP/SMEG_PLUS_512/vxWorks.bin is a raw PowerPC image
that begins with a function prologue at offset 0 and carries a VxWorks symbol table at file
offset 0x622024 (address 0x00822024): 13 854 entries of 20 bytes holding a pointer to the
name and then the address. Read at a load base of 0x00200000 the table's name pointers resolve,
and it names both addresses this page uses: 0x00484a94 is logMsg, and the application's own
call into the kernel at 0x0058c248 — the one IsAUXSRCAvailable() makes on its failure path —
is tickGet. See the kernel symbol table.
| build | sink | original | patched |
|---|---|---|---|
NAV |
0x010346d0 |
38 60 00 00 (li r3,0) |
4b 45 03 c4 (b 0x00484a94) |
AUDIO_BT, AUDIO_BT_256 |
0x01034578 |
38 60 00 00 |
4b 45 05 1c (b 0x00484a94) |
The displacement is about −11.7 MB, inside the 24-bit branch range, so no code cave is needed.
The blr after the patched instruction becomes unreachable, which is harmless: logMsg
returns to Log_msg's caller itself. Emulated, the patched sink jumps to 0x00484a94 and the
emulator reports an unmapped fetch — expected, since that address is in the kernel rather than
the application image. That confirms the branch target and nothing more (executed).
diagnostic-logging — point the sink at Log_msg (do not use with the mask)¶
dummyLogMsg at 0x010346d0 is literally:
This patch replaces that instruction with a branch to the real logger:
| build | address | original | patched |
|---|---|---|---|
NAV |
0x010346d0 |
38 60 00 00 (li r3,0) |
49 70 de 88 (b 0x02742558) |
Log_msg wants r3 = level and r4 = format, and the caller has already set both, so the
branch passes them straight through. The two functions are 24 174 216 bytes apart, inside the
24-bit branch range.
On its own it emits nothing: every redirected site lands in Log_msg and returns at the mask
gate. With diagnostic-logmask it is worse: 0x010346d0 is the sink Log_msg itself
calls, so Log_msg calls the sink, the sink re-enters Log_msg, and so on, on every log call
in the firmware. Emulated, one call re-enters Log_msg three times before unwinding
(executed). Use diagnostic-logsink for the sink instead.
Adding your own patches¶
patches/*.json is data-driven:
{
"name": "my-patch",
"description": "what it does, why, and what has been verified",
"summary": "one line of Markdown for the status table",
"status": {"state": "never-flashed", "label": "Never flashed", "note": "decoded only"},
"variants": {
"NAV": {
"app_image": "NAV/AppBin/f_BigQuick.bin",
"inf": "NAV/AppBin/f_BigQuick.bin.inf",
"smeg_inf": "NAV/smeg.inf",
"ctrl": "NAV_ctrl.bin",
"base": "0x01000000",
"patches": [
{
"addr": "0x02247858",
"expect": "9421ffa0",
"bytes": "386000014e800020",
"why": "...",
"disasm": "li r3, 1 ; blr"
}
]
}
}
}
expect is checked before writing, so a mismatched firmware build fails loudly instead of
being corrupted.
status.state is one of confirmed, flashed, never-flashed, falsified or diagnostic.
After adding a set, or when a car test changes its status, run python3 tools/patch_status.py
to regenerate the table above and the landing-page panel; a test fails while they are stale.
data — an edit that is not code¶
Every edit is checked to decode as whole PowerPC instructions. An edit to a string or a table,
such as the ring tone names media.names generates, sets "data": true to skip that check;
expect is still verified first.
disasm — pin the instructions, not just the bytes¶
disasm is optional and asserts what the patched site must decode to. Without it the tool
still disassembles every patched site and prints it, and still asserts the bytes decode to
whole, valid PowerPC instructions — so a bytes string that is corrupt or truncated is
caught. disasm goes further and pins the exact intent, which matters because the hex is
unreadable at a glance and why is prose a machine cannot check:
Anything that does not match stops the run.
Disassembly needs capstone, which is in the dev
extra. The check is skipped, not faked, when it is absent — this tool declares no
dependencies and has to keep running on a machine with nothing but the standard library, so
it returns nothing rather than passing silently.
The run verifies what it wrote¶
patch_smeg.py does not trust its own output. After writing, it re-reads every file from
disk and closes the loop:
- re-inflates the packed
f_BigQuick.binand confirms each patched site holds the new bytes, - recomputes the CRC32 of each file and checks the
.infsidecars declare it, - checks the module
ctrlrecords those CRCs, and thatctrl.binrecords the module's,
then prints the whole chain:
crc chain verified end to end
f_BigQuick.bin 0x7ce25274
f_BigQuick.bin.inf 0x0fd24c96
smeg.inf 0xcce0fb84
NAV_ctrl.bin 0xd3a50ee5
ctrl.bin name=verified (1)
In-memory checks alone would miss a write that did not land, or landed twice.
Resulting file checksums¶
Because the compressed stream is rebuilt, the resulting f_BigQuick.bin CRCs depend on
the zlib implementation/level and are not stable values to match against. Recompute them
with the tooling and propagate through the cascade (the tool does this automatically).