Acknowledged
CIQQA-4579

Simulator segfaults on Communications.transmit over the tethered (ADB) connection — SDK 9.1.0 and 9.2.0, Linux

Summary

With a tethered (ADB) data connection established, the Connect IQ simulator crashes with SIGSEGV the moment a watch app calls Communications.transmit. No byte is written to the socket. Reproducible with a one-line addition to your own samples/Comm, on both SDK 9.1.0 and 9.2.0.

The opposite direction — phone → watch — works correctly over the very same connection, so the transport and the session are demonstrably healthy when the crash happens.

Environment

  • SDK 9.1.0 (2026-03-09) and 9.2.0 (2026-06-09) — both affected
  • Linux (Ubuntu 22.04 container on a Fedora host)
  • Devices tried: vivoactive3 (Comm sample), fr965 (our own app)
  • Peer: a plain TCP server on 127.0.0.1:7381, in place of adb forward tcp:7381 tcp:7381

Minimal reproduction

  1. Listen on TCP 127.0.0.1:7381. Any server will do — this is the host side of adb forward tcp:7381 tcp:7381, and the simulator is the client. Keep it listening for the whole session.
  2. Start the simulator, then Connection → start the ADB connection. The simulator connects.
  3. Take samples/Comm unmodified and add one line to CommApp.onStart:
public function onStart(state as Dictionary?) as Void {
    Communications.transmit("Hello World.", null, new $.CommListener());
}
  1. Run it.

Expected: the payload is serialized and written to the socket.
Actual: the simulator dies with SIGSEGV within a second. Zero bytes are written.

Left unmodified — the sample only transmits from its menu — the same binary stays alive indefinitely while connected.

What we verified is NOT the cause

Each of these was tested and crashes identically:

  • Payload type — String (Comm sample) vs Dictionary (our app): both crash.
  • The options argument — null (as your sample passes) vs {} (as the docs prescribe): both crash.
  • Stale or invalid session — transmit on a session where a phone → watch message had just been accepted and displayed: crashes.
  • Our own code — reproduces on samples/Comm plus one line.
  • A recent regression — 9.1.0 and 9.2.0 both crash.

Crash analysis

Identical stack across our app and the patched sample (stripped binary, offsets from the ELF base):

#0  simulator + 0xd5fcc8
#1  simulator + 0x40d967
#2  simulator + 0x396ba6

The faulting function:

movl $0x0,(%rsi)      ; *out = 0
test %rdi,%rdi
je   <return>         ; guard 1: null object -> clean return
call <...>            ; ~ buf->data()
mov  %rax,%r12
call <...>            ; ~ buf->size()
cmp  $0x3,%eax
jbe  <bail>           ; guard 2: size <= 3 -> clean return
=> movzwl (%r12),%edx ; SIGSEGV
   movzbl 0x2(%r12),%ecx
   rol   $0x8,%dx
   shl   $0x8,%edx
   or    %ecx,%edx    ; 24-bit big-endian length

Both guards pass — the object is non-null and reports size > 3 — yet %r12 holds 0x8e432fa067e9a719, which is not a mapped address. data() therefore returns a wild pointer while size() returns a plausible value: the buffer object is in an inconsistent state (use-after-free, or read before initialisation). Nothing has been received on the socket at that point, so this is not parsing of peer input.

The opposite direction works, which makes this a narrow bug

To be certain the harness was faithful, we implemented the phone side using the serialization classes shipped in your own companion SDK (ciq-companion-app-sdk, com.garmin.monkeybrains.serialization). Sending a correctly serialized object to the simulator over the same socket works: registerForPhoneAppMessages fires and samples/Comm displays the payload, and the connection stays open.

So: phone → watch is fine; watch → phone segfaults.

Additional observations (same transport, probably related, listed for completeness)

  • If the peer stops listening, the simulator crashes rather than reporting a dropped link.
  • After a few connect/reject cycles, the simulator dies with SIGABRT instead (stack entirely in libc) — a different site.
  • Occasionally the simulator freezes instead of crashing (no coredump, the process must be killed).
  • On both SDKs, the bin/connectiq launcher aborts (SIGABRT) at startup on this machine, while running bin/simulator directly works. Possibly unrelated; mentioned in case it helps.
  • A malformed payload sent phone → watch is answered by an immediate connection close. We believe this explains the long-standing "Get Comm sample working" report ("ADB Disconnected" when messaging the watch): the payload was not a valid serialized object, and the close is the simulator’s rejection — not a transport failure.

Impact

Phone ↔ watch messaging cannot be exercised in the simulator in the watch → phone direction. For developers targeting devices they do not own, the simulator is the only option, so this blocks testing of any app that talks to a companion app.

Parents
  • A few more measurements since the original report, in case they help narrow the fault. Same environment: SDK 9.2.0, Linux, tested on venu3 and fr965.

    1. The receive path is healthy, and stays healthy, on the very same connection

    With a correctly serialised object pushed phone → watch over the tethered link, the watch app receives it, renders it, and the connection stays ESTABLISHED indefinitely. I drove this from a plain Python TCP server standing in for the phone — no Android, no companion app — and the watch rendered the pushed list.

    So the session is demonstrably alive at the moment transmit takes the process down. Whatever is wrong is on the send path, not in the connection or the handshake.

    2. A malformed payload is answered by an immediate RST, with no diagnostic

    Sending 35 bytes of plain JSON — rather than a serialised object — over an established connection produces ECONNRESET on the peer within the same second. Not a clean close, not an error, not a log line: the socket is simply torn down. The simulator itself survives.

    Two things follow. First, the receive path can clearly tell a valid payload from an invalid one, so the teardown is a decision, not a parse crash. Second, from the phone's side an RST is indistinguishable from adb dropping the link, so it sends developers hunting in the transport layer for a problem that is in their encoder. An error, or even a silent discard, would cost them a lot less than a reset. I would file that as a separate usability bug if you would find it useful.

    Worth noting how uninformative the symptom is: the same « ADB Disconnected » is reported by someone running your OOTB Comm sample on both ends — so their payload came from sendMessage and was serialised by the SDK. Their cause is not this one, and remains unknown after six years. That one alert covers at least two unrelated faults.

    3. The harness is faithful to what a real companion app writes

    To be sure I was not testing my own misunderstanding, I proxied a real Android companion app against the simulator and captured what the SDK actually puts on the wire. Decoding those bytes gives the expected values, and re-encoding them reproduces the capture byte for byte. The messages carry no length prefix — objects are self-delimiting and written back to back.

    In other words, the payloads in the original report are the same shape as the SDK's own.

    4. One more, on the companion side

    Not your bug, but it costs anyone building a tethered rig an afternoon, so it belongs next to this report: in IQConnectType.TETHERED, ConnectIQ.sendMessage writes to the socket synchronously, on the calling thread. Called from the main looper — which is what you naturally do when answering an inbound message — Android throws NetworkOnMainThreadException and the message is lost silently. In WIRELESS it never happens, because that strategy hands off to Garmin Connect Mobile instead of touching a socket. It would be kind to mention the threading requirement in the companion SDK docs.

    Workaround, and tools

    Until transmit is fixed, the watch → phone direction can be routed through System.println behind a build annotation and read off the monkeydo console. It is ugly, but it restores a full-duplex rig on a device one does not own.

    I have published the pieces under Apache-2.0, so nobody else has to rediscover any of this: https://github.com/lgnap/connectiq-tethered-rig — a Python implementation of the serialisation format, a fake phone that impersonates the peer on 7381, and the console relay. It contains no Garmin code; the SDK is yours to supply.

    Happy to run further experiments on this setup if any specific measurement would help.

Comment
  • A few more measurements since the original report, in case they help narrow the fault. Same environment: SDK 9.2.0, Linux, tested on venu3 and fr965.

    1. The receive path is healthy, and stays healthy, on the very same connection

    With a correctly serialised object pushed phone → watch over the tethered link, the watch app receives it, renders it, and the connection stays ESTABLISHED indefinitely. I drove this from a plain Python TCP server standing in for the phone — no Android, no companion app — and the watch rendered the pushed list.

    So the session is demonstrably alive at the moment transmit takes the process down. Whatever is wrong is on the send path, not in the connection or the handshake.

    2. A malformed payload is answered by an immediate RST, with no diagnostic

    Sending 35 bytes of plain JSON — rather than a serialised object — over an established connection produces ECONNRESET on the peer within the same second. Not a clean close, not an error, not a log line: the socket is simply torn down. The simulator itself survives.

    Two things follow. First, the receive path can clearly tell a valid payload from an invalid one, so the teardown is a decision, not a parse crash. Second, from the phone's side an RST is indistinguishable from adb dropping the link, so it sends developers hunting in the transport layer for a problem that is in their encoder. An error, or even a silent discard, would cost them a lot less than a reset. I would file that as a separate usability bug if you would find it useful.

    Worth noting how uninformative the symptom is: the same « ADB Disconnected » is reported by someone running your OOTB Comm sample on both ends — so their payload came from sendMessage and was serialised by the SDK. Their cause is not this one, and remains unknown after six years. That one alert covers at least two unrelated faults.

    3. The harness is faithful to what a real companion app writes

    To be sure I was not testing my own misunderstanding, I proxied a real Android companion app against the simulator and captured what the SDK actually puts on the wire. Decoding those bytes gives the expected values, and re-encoding them reproduces the capture byte for byte. The messages carry no length prefix — objects are self-delimiting and written back to back.

    In other words, the payloads in the original report are the same shape as the SDK's own.

    4. One more, on the companion side

    Not your bug, but it costs anyone building a tethered rig an afternoon, so it belongs next to this report: in IQConnectType.TETHERED, ConnectIQ.sendMessage writes to the socket synchronously, on the calling thread. Called from the main looper — which is what you naturally do when answering an inbound message — Android throws NetworkOnMainThreadException and the message is lost silently. In WIRELESS it never happens, because that strategy hands off to Garmin Connect Mobile instead of touching a socket. It would be kind to mention the threading requirement in the companion SDK docs.

    Workaround, and tools

    Until transmit is fixed, the watch → phone direction can be routed through System.println behind a build annotation and read off the monkeydo console. It is ugly, but it restores a full-duplex rig on a device one does not own.

    I have published the pieces under Apache-2.0, so nobody else has to rediscover any of this: https://github.com/lgnap/connectiq-tethered-rig — a Python implementation of the serialisation format, a fake phone that impersonates the peer on 7381, and the console relay. It contains no Garmin code; the SDK is yours to supply.

    Happy to run further experiments on this setup if any specific measurement would help.

Children
  • I can reproduce the same crash on Linux with the ADB tethered connection enabled, and Communications.transmit triggers a SIGSEGV immediately in both SDK 9.1.0 and 9.2.0. This looks like a simulator regression in the ADB transport path rather than an application-level issue, so the acknowledgement is appreciated.