Skip to content

fw/services: answer a put_bytes install with its own token - #1987

Open
zunda-pixel wants to merge 1 commit into
coredevices:mainfrom
zunda-pixel:put-bytes-install-token
Open

fw/services: answer a put_bytes install with its own token#1987
zunda-pixel wants to merge 1 commit into
coredevices:mainfrom
zunda-pixel:put-bytes-install-token

Conversation

@zunda-pixel

@zunda-pixel zunda-pixel commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What

The response to a PutBytes INSTALL (0x05) carries token 0 instead of the token the phone sent. Both the ACK and the NACK paths now answer with the install's own token.

Why

prv_cleanup_and_send_response reads s_pb_state.token, but the COMMIT (0x03) that always precedes an install has already gone through prv_cleanup, which resets the transfer state:

// NOTE: Preserve the type field because that is checked by the install handler after we
//  cleanup (cleanup is called after a commit).
PutBytesObjectType type = s_pb_state.type;
s_pb_state = (PutBytesState) { .type = type };

That NOTE is the bug in miniature: the "install runs after cleanup" invariant was already known, type was carried across it, and token was not.

Zero is guaranteed rather than racy — prv_is_valid_command_for_current_state admits PutBytesInstall only from PutBytesIdle, so an install is always answered with a torn-down state. The value that should be echoed was in hand the whole time: prv_do_install(token) looks the phone's token up in s_ready_to_install[] and NACKs a mismatch.

A phone that matches a response to the request it sent therefore never sees the install finish: the transfer reaches 100 %, the watch writes the object, and the update hangs.

How it was found

Observed on a Pebble Time 2 (obelix_pvt) running v4.36.2, from a third-party iOS companion app I maintain, which has to ignore the token on this one message to get past it.

libpebble3 hit the same wall — the cookie check in PutBytesSession.sendInstall is commented out with // TODO this fired?, while sendPut and sendCommit keep theirs. coredevices/mobileapp#398 restores it.

Tests

Two regression tests added to tests/fw/services/test_put_bytes.c, one per path:

  • install_ack_carries_the_install_token
  • install_nack_carries_the_install_token

Reverting only the src/ hunk makes both fail with the reported symptom (0 NOT == <token>), so they test the bug rather than the new code. Locally, ctest --test-dir build-test -R put_bytes:

1/2 Test #158: test_put_bytes_obelix ...  Passed  1.63 sec
2/2 Test #159: test_put_bytes_gabbro ...  Passed  1.46 sec
100% tests passed out of 2

Notes

  • No behaviour change for a phone that ignores the token; a phone that checks it starts working.
  • This also makes install responses self-consistent: the state-error path already echoed the request token through prv_fail, while the success and unknown-token paths answered 0.
  • prv_cleanup_and_send_response keeps its signature and callers; the new _with_token variant is used only by the two install paths. Preserving token across prv_cleanup the way type is preserved would have leaked stale state into every other path that reads s_pb_state.token.

AI use

Written with Claude Code (Claude Opus 5); disclosed by the Co-Authored-By trailer on the commit, per CONTRIBUTING's generative AI section. I have reviewed the change and the firmware paths it describes, and am happy to answer questions about either.

Seen end to end in QEMU

Built qemu_emery twice, differing only in this hunk, and ran the same firmware install against each from a client that logs the token of every put_bytes answer. Run-length encoded, since a transfer acknowledges every chunk with the same token:

This branch

acknowledgement/token=580448907  ×854   <- firmware transfer
acknowledgement/token=1686229799 ×510   <- resources transfer
acknowledgement/token=580448907         <- answer to install(580448907)
acknowledgement/token=1686229799        <- answer to install(1686229799)
install finished

main

acknowledgement/token=1999088414 ×854
acknowledgement/token=776954018  ×510
acknowledgement/token=0
install ended with: connectionTimedOut

So each install is answered with its own token rather than with zero, which is what this changes.

One thing in the main run I cannot account for and would rather flag than explain away: only the first install was answered at all, and the second drew nothing, so the client gave up after its 10s deadline. A real Pebble Time 2 on v4.36.2 answered both installs 32ms apart in the same client's log earlier today, so the emulator and the hardware do not agree here and I do not know which way the difference runs. It has no bearing on the token, which is what the two host tests pin and what the diff is about.

The response to a PutBytes INSTALL carried token 0 instead of the token
the phone sent. prv_cleanup_and_send_response reads s_pb_state.token, and
the COMMIT that always precedes an install has already run prv_cleanup,
which resets the transfer state. prv_do_install validates the phone's
token against s_ready_to_install, so the right value was in hand — it
simply never reached the response.

Zero was guaranteed rather than racy: prv_is_valid_command_for_current_state
admits PutBytesInstall only from PutBytesIdle, so an install is always
answered with a torn-down transfer state. prv_cleanup already preserves
type across that teardown, with a NOTE saying it does so because the
install handler reads it afterwards; the token needed the same care and
did not get it.

A phone that matches a response to the request it sent therefore never
sees the install finish: the transfer reaches 100%, the watch writes the
object, and the update hangs. Observed on a Pebble Time 2 (obelix_pvt)
running v4.36.2 from a third-party companion app, which now has to ignore
the token on this one message.

Both the ACK and the NACK paths answer with the install's own token now,
which also makes install responses self-consistent: the state-error path
already echoed the request token through prv_fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: zunda pixel <zunda.dev@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant