Хочу написать про один странный креш, с которым разбирался на работе.
Креш происходил стабильно при заходе в папку с корейскими символами. Проблема оказалась во вроде бы безобидном коде следующего вида:
NSURLComponents* urlComp = [[NSURLComponents new] autorelease];
...
urlComp.path = path;
urlComp.user = username;
...
Падает при выставлении user — EXC_BAD_ACCESS внутри сеттера при посылке objc_msgSend кому-то. Все переменные в порядке, ничто не могло сломаться. При этом креш воспроизводится в релизной конфигурации, но не в дебажной. Ругаясь на плохую работу отладчика в релизной конфе, идем смотреть дальше.
Хоть отладчик зачастую и не может в релизе распечатать переменные, но по дизассемблерному листингу легко видеть в каком регистре, какие переменные должны быть, и отладчик способен нормально выводить объекты (например, po $r0). Быстро становится понятно, что сдох username (в моем случае регистр r10) — po $r10 выводит число, а не объект. Несколько менее быстро становится понятно, что значение в регистре r10 поменялось после выставления path.
Окей, лезем смотреть, что происходит в методе "-[__NSConcreteURLComponents setPath:]". Благо дело он небольшой и видно, что регистр r10 слетает при вызове "-[NSString(NSURLUtilities) stringByAddingPercentEncodingWithAllowedCharacters:]" — т.е. когда эскейпится переданный путь. Эта функция уже большая, и заказчик голову отровет за ее анализ, но хотя бы глянем на вход-выход
0x2ca50aec: push.w {r8, r10, r11}
0x2ca50af0: sub.w sp, sp, #0x1020
0x2ca50af4: sub sp, #0x10
...
0x2ca50e7a: add.w sp, sp, #0x1020
0x2ca50e7e: add sp, #0x10
0x2ca50e80: pop.w {r8, r10, r11}
При входе наш r10 сохраняется в стек, а при выходе восстанавливается. Указатель стека (sp) в порядке, что было, то и вернулось, а вот само содержимое стека уже не то — значение r10 восстановилось неверно. Таким образом, в системной функции для percent encoding'a есть повреждение стека.
Для наглядности я вынес код-пример в чистый тестовый проект:
NSObject* obj1 = [[NSObject new] autorelease];
NSObject* obj2 = [[NSObject new] autorelease];
NSObject* obj3 = [[NSObject new] autorelease];
NSObject* obj4 = [[NSObject new] autorelease];
NSString* str = @"/Users/zaryanov/Movies/rootfolder/시티 오브 히어로 (City of Heroes)/로니 리 가드너 (1961년부터 2010년까지)는 1985 년에 살인죄로 사형을받은 유타 주에서 총살형 된 미국의 악당이었다. 1984 년에 그는 솔트 레이크 시티에서 강도 동안 바텐더를 살해.m4v";
NSLog(@"%s str %@", __func__, str);
NSCharacterSet* charSet = [NSCharacterSet URLPathAllowedCharacterSet];
str = [str stringByAddingPercentEncodingWithAllowedCharacters:charSet];
NSLog(@"%s str %@", __func__, str);
NSLog(@"%s obj1 %@ obj2 %@ obj3 %@ obj4 %@", __func__, obj1, obj2, obj3, obj4);
При этом креш произошел не при выводе в лог, как я ожидал, а в самой проблемной функции (эскейпинга). Сработал abort в функции __stack_chk_fail — дело в том, что в чистом тестовом проекте была разрешена архитектура arm64, и там, по всей видимости, есть проверка стека. Если оставить только armv7, то креш происходит при выводе объектов в лог, как я и ожидал. В любом случае, стек поврежден.
Далее гугление по «stringByAddingPercentEncodingWithAllowedCharacters crash» дает некоторые подтверждающие результаты:
https://github.com/Alamofire/Alamofire/issues/206 — здесь, правда, жалуются на большое потребление памяти, но функция та же;
https://gist.github.com/clowwindy/0d800f07a5e95e5c4dd0 — здесь пример, который сносит стек совсем, а не пару регистров.
Собственно, из первой приведенной ссылки я и взял логичное решение — юзать CFURLCreateStringByAddingPercentEscapes, менее удобно, зато работает. Тому же NSURLComponents можно выставить самим уже заэскейпленный путь.
Проблема воспроизводится на iOS 8.2, так что стоит оставить себе зарубку на подкорке. Как видно в моем случае, креш может быть немного спрятан и не очевиден из-за неявного вызова проблемной функции через другую функцию. Ну и с повреждением стека может повезти по-разному, если зацепит только регистры, то это может далеко не сразу обнаружиться.
Автор: PavelTretyakov