Pull to refresh

Повреждение стека в одном из методов NSString

Reading time 3 min
Views 6.5K
Хочу написать про один странный креш, с которым разбирался на работе.

Креш происходил стабильно при заходе в папку с корейскими символами. Проблема оказалась во вроде бы безобидном коде следующего вида:

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, так что стоит оставить себе зарубку на подкорке. Как видно в моем случае, креш может быть немного спрятан и не очевиден из-за неявного вызова проблемной функции через другую функцию. Ну и с повреждением стека может повезти по-разному, если зацепит только регистры, то это может далеко не сразу обнаружиться.

UPD:
Не найдя такого бага в openradar, решил запостить его. При этом скопировал пример (не из браузера, но в нем может быть тот же результат) и… не получил креша. Оказалось, что эти иероглифы могут быть закодированы в UTF-8 разными способами, и в одном случае падает, а в другом — нет. Так, иероглиф 시 может быть закодирован как составной из двух элементов (получится 6 байт — E1 84 89 E1 85 B5), а может быть закодирован как уже собранный (получится 3 байта — EC 8B 9C). Во втором случае с такой строкой не упало.

Баг запостил — http://www.openradar.me/20404230, там же есть ссылка на пример в github, где баг воспроизводится.
Tags:
Hubs:
+17
Comments 10
Comments Comments 10

Articles