Skip to content

[iOS] EXC_BAD_ACCESS crash during printer discovery (TM-m30, LAN mode) #248

@zameschua

Description

@zameschua

I'll be honest, I'm a React Native developer and not an iOS developer, the issue was solved with Claude Code. I've tested the fix and it works flawlessly on my devices.

--- Issue description below generated by Claude Code ---

Summary

On iOS, the app crashes with EXC_BAD_ACCESS during printer discovery when using Epson TM-m30 printers over LAN. The crash is caused by a race condition in the onDiscovery: callback in EscPosPrinterDiscovery.mm where the mutable _printerList array is passed directly to the React Native bridge, which may serialize it asynchronously while another callback mutates it.

This issue is iOS-specific. We have not observed this crash on Android.

Root Cause Analysis

After extensive debugging, we identified the root cause in the react-native-esc-pos-printer integration code (ios/EscPosPrinterDiscovery.mm), not the Epson ePOS SDK itself:

  1. The Epson ePOS SDK fires onDiscovery: delegate callbacks from background threads (this is expected SDK
    behavior)
  2. The current integration code passes _printerList (an NSMutableArray) directly to the React Native
    bridge via sendEventWithName:body:
  3. The React Native bridge may serialize this array asynchronously after the method returns
  4. While serialization is in progress, another callback can mutate _printerList, causing concurrent
    modification and EXC_BAD_ACCESS

Note: The Epson ePOS SDK is a closed-source binary. The fix is in the Objective-C++ wrapper code that bridges SDK callbacks to React Native, ensuring thread-safe handling of the mutable array.

Evidence: Adding extensive NSLog statements (which block the thread during I/O) prevented the crash, confirming the timing-sensitive nature of the race condition.

Proposed Fix

Changes to ios/EscPosPrinterDiscovery.mm:

- (void) onDiscovery:(Epos2DeviceInfo *)deviceInfo
{
    // Copy all values from deviceInfo IMMEDIATELY on the callback thread.
    // Use [copy] to create our own string objects in case SDK reuses internal buffers.
    NSString *target = [[deviceInfo getTarget] copy] ?: @"";
    NSString *deviceName = [[deviceInfo getDeviceName] copy] ?: @"";
    NSString *ipAddress = [[deviceInfo getIpAddress] copy] ?: @"";
    NSString *macAddress = [[deviceInfo getMacAddress] copy] ?: @"";
    NSString *bdAddress = [[deviceInfo getBdAddress] copy] ?: @"";
    int deviceType = [deviceInfo getDeviceType];

    NSDictionary *deviceDict = @{
        @"target": target,
        @"deviceName": deviceName,
        @"ipAddress": ipAddress,
        @"macAddress": macAddress,
        @"bdAddress": bdAddress,
        @"deviceType": @(deviceType)
    };

    // Static lock to serialize access to _printerList
    static NSLock *discoveryLock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        discoveryLock = [[NSLock alloc] init];
    });

    // Lock, mutate array, take immutable snapshot, unlock
    [discoveryLock lock];
    [_printerList addObject:deviceDict];
    // CRITICAL: Create immutable copy for the bridge.
    // This prevents crashes from concurrent modification if the bridge
    // serializes asynchronously while another callback mutates _printerList.
    NSArray *printerListSnapshot = [_printerList copy];
    [discoveryLock unlock];

    // Emit event with the immutable snapshot on main queue.
    dispatch_async(dispatch_get_main_queue(), ^{
        #if RCT_NEW_ARCH_ENABLED
        [self emitOnDiscovery:printerListSnapshot];
        #else
        [self sendEventWithName:@"onDiscovery" body:printerListSnapshot];
        #endif
    });
}

Key changes:

  1. Immediate string copying - Use [copy] on all strings from deviceInfo in case the SDK reuses internal
    buffers
  2. NSLock for thread safety - Serialize access to the mutable _printerList
  3. Immutable snapshot - Pass [_printerList copy] to the bridge instead of the mutable array
  4. Async dispatch to main queue - Safe because the snapshot is immutable

Related Issues

This may be related to #129 which reports similar EXC_BAD_ACCESS crashes during discovery.

Workaround

For anyone experiencing this issue, you can use pnpm patch (or patch-package for npm/yarn) to apply the fix
above to ios/EscPosPrinterDiscovery.mm.

Steps to reproduce

  1. Build and run the app on an iOS device
  2. Have an Epson TM-m30III printer connected via LAN
  3. Start printer discovery using startDiscovery()
  4. The app crashes intermittently with EXC_BAD_ACCESS

Note: The crash can occur with just one printer on the network, but the frequency increases with multiple printers.

react-native-esc-pos-printer version

4.5.0

React Native version

0.81.5

Platforms

iOS

Workflow

Expo Dev Client

Architecture

Fabric (New Architecture)

Build type

Release app & production bundle

Device

Real device

Device model

iPad (A16), iPadOS 26.2.1

Acknowledgements

Yes

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