Skip to content

DTX Connection Breaks on iOS 14+ Due to Plist Parse Errors #661

@oslo254804746

Description

@oslo254804746

Summary
The DTX connection terminates prematurely on iOS 14+ devices when using custom MessageDispatcher implementations. After successfully receiving the first 3-4 messages, the connection closes with a plist parsing error, preventing further data collection.
Problem Description
When implementing FPS monitoring or other continuous data streaming services on iOS 14+ devices, the reader() goroutine in connection.go encounters messages that cannot be parsed as valid plist format. The current implementation treats all ReadMessage() errors as fatal and closes the entire connection, even though these parse errors are non-fatal and the underlying network connection is still healthy.
Error logs:

DTX Connection error: plist: error parsing text property list: missing = in dictionary at line 0 character 2
error reading dtx connection plist: error parsing text property list: missing = in dictionary at line 0 character 2

After this error, the reader() goroutine exits, the connection closes, and no further messages can be received despite the service still running on the device.
Impact

Severity: High - Makes continuous monitoring impossible on iOS 14+
Affected iOS versions: iOS 14.0 and later
Unaffected: iOS 13 and earlier work perfectly
Affected services: Primarily com.apple.instruments.server.services.coreprofilesessiontap and other streaming data services

Reproduction Steps

Setup: Connect to an iOS 14+ device with developer mode enabled
Create a custom dispatcher for handling kperf data:

type customDispatcher struct {
    conn *dtx.Connection
}

func (d *customDispatcher) Dispatch(m dtx.Message) {
    // Send ACK if needed
    dtx.SendAckIfNeeded(d.conn, m)
    
    // Process message data
    if m.Auxiliary.GetArguments() != nil {
        // Handle kperf data from auxiliary
    }
}

Connect to instruments service:

conn, err := dtx.NewUsbmuxdConnection(device, "com.apple.instruments.remoteserver.DVTSecureSocketProxy")
if err != nil {
    log.Fatal(err)
}

dispatcher := &customDispatcher{conn: conn}
conn.MessageDispatcher = dispatcher

Request CoreProfileSessionTap channel and start streaming:

channel := conn.RequestChannelIdentifier(
    "com.apple.instruments.server.services.coreprofilesessiontap", 
    dispatcher,
)

// Configure and start
config := map[string]interface{}{
    "ur": 500,
    "rp": 10,
    "tc": []interface{}{
        map[string]interface{}{
            "kdf2": []interface{}{630784000, 833617920, 830472456},
            "tk":   3,
            "uuid": uuid.New().String(),
        },
    },
}

channel.MethodCall("setConfig:", config)
channel.MethodCall("start")

Observe the failure:

First 3-4 messages arrive successfully with kperf data
Connection then breaks with plist parse error
No further messages are received
Auto-reconnect attempts show the same pattern

Root Cause Analysis
In ios/dtx_codec/connection.go, the reader() function (around line 120-150):

func reader(dtxConn *Connection) {
    reader := bufio.NewReader(dtxConn.deviceConnection.Reader())
    for {
        msg, err := ReadMessage(reader)
        if err != nil {
            defer dtxConn.close(err)  // ← Closes on ANY error
            errText := err.Error()
            log.Infof("DTX Connection error: %s", errText)
            if err == io.EOF || strings.Contains(errText, "use of closed network") {
                log.Debug("DTX Connection with EOF")
                return
            }
            log.Errorf("error reading dtx connection %+v", err)
            return  // ← Exits reader goroutine, connection is dead
        }
        // ... dispatch message
    }
}

The problem: iOS 14+ occasionally sends messages that aren't valid plist format. These could be:

Protocol-level control messages
Binary data with different encoding
Device-specific communication that varies by iOS version

The current code treats these as fatal errors and terminates the connection, when it should skip the malformed message and continue reading.
Working Solution
I've verified this fix works perfectly

func reader(dtxConn *Connection) {
    reader := bufio.NewReader(dtxConn.deviceConnection.Reader())
    consecutiveErrors := 0
    const maxConsecutiveErrors = 5
    
    for {
        msg, err := ReadMessage(reader)
        if err != nil {
            errText := err.Error()
            
            // Fatal: actual connection failure
            if err == io.EOF || strings.Contains(errText, "use of closed network") {
                log.Debug("DTX Connection with EOF")
                defer dtxConn.close(err)
                return
            }
            
            // Non-fatal: plist parse error - skip and continue
            if strings.Contains(errText, "plist") || 
               strings.Contains(errText, "parsing text property list") {
                consecutiveErrors++
                log.Warnf("Skipping malformed message (plist parse error), consecutive errors: %d", consecutiveErrors)
                
                // Safety: if too many consecutive errors, connection might be truly broken
                if consecutiveErrors >= maxConsecutiveErrors {
                    log.Errorf("Too many consecutive parse errors, closing connection")
                    defer dtxConn.close(err)
                    return
                }
                
                // Give buffer time to resync
                time.Sleep(10 * time.Millisecond)
                continue  // Skip this message, read next one
            }
            
            // Unknown error: treat as fatal
            log.Errorf("error reading dtx connection %+v", err)
            defer dtxConn.close(err)
            return
        }
        
        // Success: reset error counter
        consecutiveErrors = 0
        
        // Dispatch as normal
        if _channel, ok := dtxConn.activeChannels.Load(msg.ChannelCode); ok {
            channel := _channel.(*Channel)
            channel.Dispatch(msg)
        } else {
            dtxConn.globalChannel.Dispatch(msg)
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions