diff --git a/AGENTS.md b/AGENTS.md index dbb129a..487ad43 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -294,6 +294,14 @@ These are the things we've burned ourselves on. Following them isn't optional. *the platform API didn't complain*. See `decisions/2026-08-09-tap-xy-reports-observed-effect.md`. +15. **Never capture `[nsstring UTF8String]` in a block that outlives the + scope.** The pointer belongs to the NSString (and is autorelease-scoped + besides), so a `const char *` captured for a callback — a `UIAlertAction` + handler, a completion block, anything the run loop calls back later — is + reading freed memory by the time it fires. Capture the object and convert + inside the block. Both alert NIFs did this, and it is invisible in testing + because freed bytes usually still spell the old string. + ## Where to look | Question | File | diff --git a/ios/mob_nif.m b/ios/mob_nif.m index 1f6740d..5c53aad 100644 --- a/ios/mob_nif.m +++ b/ios/mob_nif.m @@ -7350,11 +7350,16 @@ static ERL_NIF_TERM nif_alert_show(ErlNifEnv *env, int argc, const ERL_NIF_TERM as = UIAlertActionStyleCancel; if ([style isEqualToString:@"destructive"]) as = UIAlertActionStyleDestructive; - const char *act_c = [action UTF8String]; + // Capture the NSString, not [action UTF8String]. The C pointer aims + // into a string owned by `buttons`, a local ARC releases the moment + // this block returns — long before anyone taps — so the handler read + // freed memory and enif_make_atom built the action atom out of + // whatever was there. Capturing the object retains it for the life + // of the handler. [ac addAction:[UIAlertAction actionWithTitle:label style:as handler:^(UIAlertAction *_) { - mob_deliver_alert_action(act_c); + mob_deliver_alert_action([action UTF8String]); }]]; } UIViewController *vc = root_vc(); @@ -7399,11 +7404,10 @@ static ERL_NIF_TERM nif_action_sheet_show(ErlNifEnv *env, int argc, const ERL_NI as = UIAlertActionStyleCancel; if ([style isEqualToString:@"destructive"]) as = UIAlertActionStyleDestructive; - const char *act_c = [action UTF8String]; [ac addAction:[UIAlertAction actionWithTitle:label style:as handler:^(UIAlertAction *_) { - mob_deliver_alert_action(act_c); + mob_deliver_alert_action([action UTF8String]); }]]; } UIViewController *vc = root_vc();