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
  • Reproed/patched on Mac, 9.2.0:

    Verify before patching

    APP="$HOME/Library/Application Support/Garmin/ConnectIQ/Sdks/connectiq-sdk-mac-9.2.0-2026-06-09-92a1605b2/bin/ConnectIQ.app"
    SIM="$APP/Contents/MacOS/simulator"
    # exact build check
    echo "391cfc8afe41137aea734cc03f31f6d645ace1f5cbafb67e96c55a1507682b3f $SIM" | shasum -a 256 -c -
    # and/or just the two words — must print f40302aa and e00302aa
    xxd -s $((0x1797448)) -l 4 -p "$SIM"; xxd -s $((0x1797458)) -l 4 -p "$SIM"

    If these don't match, STOP, your version is not the same

    Patch

    cp -R "$APP" "$APP.orig"
    printf '\x54\x08\x40\xf9' | dd of="$SIM" bs=1 seek=$((0x1797448)) conv=notrunc
    printf '\xe0\x03\x14\xaa' | dd of="$SIM" bs=1 seek=$((0x1797458)) conv=notrunc
    
    # confirm — must print 540840f9 and e00314aa
    xxd -s $((0x1797448)) -l 4 -p "$SIM"; xxd -s $((0x1797458)) -l 4 -p "$SIM"

    Re-sign

    codesign --force --sign - "$APP/Contents/MacOS/libANT.dylib"
    codesign --force --sign - "$APP"

    (libAnt.dylib is Garmin-signed and depended on by the app so needs to be resigned as well)

    I tested this works with a basic hello world companion app connected to the simulator ADB, no guarantees beyond that.

Comment
  • Reproed/patched on Mac, 9.2.0:

    Verify before patching

    APP="$HOME/Library/Application Support/Garmin/ConnectIQ/Sdks/connectiq-sdk-mac-9.2.0-2026-06-09-92a1605b2/bin/ConnectIQ.app"
    SIM="$APP/Contents/MacOS/simulator"
    # exact build check
    echo "391cfc8afe41137aea734cc03f31f6d645ace1f5cbafb67e96c55a1507682b3f $SIM" | shasum -a 256 -c -
    # and/or just the two words — must print f40302aa and e00302aa
    xxd -s $((0x1797448)) -l 4 -p "$SIM"; xxd -s $((0x1797458)) -l 4 -p "$SIM"

    If these don't match, STOP, your version is not the same

    Patch

    cp -R "$APP" "$APP.orig"
    printf '\x54\x08\x40\xf9' | dd of="$SIM" bs=1 seek=$((0x1797448)) conv=notrunc
    printf '\xe0\x03\x14\xaa' | dd of="$SIM" bs=1 seek=$((0x1797458)) conv=notrunc
    
    # confirm — must print 540840f9 and e00314aa
    xxd -s $((0x1797448)) -l 4 -p "$SIM"; xxd -s $((0x1797458)) -l 4 -p "$SIM"

    Re-sign

    codesign --force --sign - "$APP/Contents/MacOS/libANT.dylib"
    codesign --force --sign - "$APP"

    (libAnt.dylib is Garmin-signed and depended on by the app so needs to be resigned as well)

    I tested this works with a basic hello world companion app connected to the simulator ADB, no guarantees beyond that.

Children