]>
Commit | Line | Data |
---|---|---|
34d5b5e8 A |
1 | /* |
2 | TEST_ENTITLEMENTS preopt-caches.entitlements | |
3 | TEST_CONFIG OS=iphoneos MEM=mrc | |
4 | TEST_BUILD | |
5 | mkdir -p $T{OBJDIR} | |
6 | /usr/sbin/dtrace -h -s $DIR/../runtime/objc-probes.d -o $T{OBJDIR}/objc-probes.h | |
7 | $C{COMPILE} $DIR/preopt-caches.mm -std=gnu++17 -isystem $C{SDK_PATH}/System/Library/Frameworks/System.framework/PrivateHeaders -I$T{OBJDIR} -ldsc -o preopt-caches.exe | |
8 | END | |
9 | */ | |
10 | // | |
11 | // check_preopt_caches.m | |
12 | // check-preopt-caches | |
13 | // | |
14 | // Created by Thomas Deniau on 11/06/2020. | |
15 | // | |
16 | ||
17 | #define TEST_CALLS_OPERATOR_NEW | |
18 | ||
19 | #include "test-defines.h" | |
20 | #include "../runtime/objc-private.h" | |
21 | #include <objc/objc-internal.h> | |
22 | ||
23 | #include <dlfcn.h> | |
24 | #include <dirent.h> | |
25 | #include <objc/runtime.h> | |
26 | #include <stdio.h> | |
27 | #include <stdlib.h> | |
28 | #include <mach-o/dyld.h> | |
29 | #include <mach-o/dyld_process_info.h> | |
30 | #include <mach-o/dyld_cache_format.h> | |
31 | #include <mach-o/dsc_iterator.h> | |
32 | #include <unordered_map> | |
33 | #include <string> | |
34 | #include <vector> | |
35 | #include <set> | |
36 | #include <spawn.h> | |
37 | #include <sys/poll.h> | |
38 | ||
39 | #include "test.h" | |
40 | ||
41 | int validate_dylib_in_forked_process(const char * const toolPath, const char * const dylib) | |
42 | { | |
43 | int out_pipe[2] = {-1}; | |
44 | int err_pipe[2] = {-1}; | |
45 | int exit_code = -1; | |
46 | pid_t pid = 0; | |
47 | int rval = 0; | |
48 | ||
49 | std::string child_stdout; | |
50 | std::string child_stderr; | |
51 | ||
52 | posix_spawn_file_actions_t actions = NULL; | |
53 | const char * const args[] = {toolPath, dylib, NULL}; | |
54 | int ret = 0; | |
55 | ||
56 | if (pipe(out_pipe)) { | |
57 | exit(3); | |
58 | } | |
59 | ||
60 | if (pipe(err_pipe)) { | |
61 | exit(3); | |
62 | } | |
63 | ||
64 | //Do-si-do the FDs | |
65 | posix_spawn_file_actions_init(&actions); | |
66 | posix_spawn_file_actions_addclose(&actions, out_pipe[0]); | |
67 | posix_spawn_file_actions_addclose(&actions, err_pipe[0]); | |
68 | posix_spawn_file_actions_adddup2(&actions, out_pipe[1], 1); | |
69 | posix_spawn_file_actions_adddup2(&actions, err_pipe[1], 2); | |
70 | posix_spawn_file_actions_addclose(&actions, out_pipe[1]); | |
71 | posix_spawn_file_actions_addclose(&actions, err_pipe[1]); | |
72 | ||
73 | // Fork so that we can dlopen the dylib in a clean context | |
74 | ret = posix_spawnp(&pid, args[0], &actions, NULL, (char * const *)args, NULL); | |
75 | ||
76 | if (ret != 0) { | |
77 | fail("posix_spawn for %s failed: returned %d, %s\n", dylib, ret, strerror(ret)); | |
78 | exit(3); | |
79 | } | |
80 | ||
81 | posix_spawn_file_actions_destroy(&actions); | |
82 | close(out_pipe[1]); | |
83 | close(err_pipe[1]); | |
84 | ||
85 | std::string buffer(4096,' '); | |
86 | std::vector<pollfd> plist = { {out_pipe[0],POLLIN,0}, {err_pipe[0],POLLIN,0} }; | |
87 | while (( (rval = poll(&plist[0],(nfds_t)plist.size(), 100000)) > 0 ) || ((rval < 0) && (errno == EINTR))) { | |
88 | if (rval < 0) { | |
89 | // EINTR | |
90 | continue; | |
91 | } | |
92 | ||
93 | ssize_t bytes_read = 0; | |
94 | ||
95 | if (plist[0].revents&(POLLERR|POLLHUP) || plist[1].revents&(POLLERR|POLLHUP)) { | |
96 | bytes_read = read(out_pipe[0], &buffer[0], buffer.length()); | |
97 | bytes_read = read(err_pipe[0], &buffer[0], buffer.length()); | |
98 | break; | |
99 | } | |
100 | ||
101 | if (plist[0].revents&POLLIN) { | |
102 | bytes_read = read(out_pipe[0], &buffer[0], buffer.length()); | |
103 | child_stdout += buffer.substr(0, static_cast<size_t>(bytes_read)); | |
104 | } | |
105 | else if ( plist[1].revents&POLLIN ) { | |
106 | bytes_read = read(err_pipe[0], &buffer[0], buffer.length()); | |
107 | child_stderr += buffer.substr(0, static_cast<size_t>(bytes_read)); | |
108 | } | |
109 | else break; // nothing left to read | |
110 | ||
111 | plist[0].revents = 0; | |
112 | plist[1].revents = 0; | |
113 | } | |
114 | if (rval == 0) { | |
115 | // Early timeout so try to clean up. | |
116 | fail("Failed to validate dylib %s: timeout!\n", dylib); | |
117 | return 1; | |
118 | } | |
119 | ||
120 | ||
121 | if (err_pipe[0] != -1) { | |
122 | close(err_pipe[0]); | |
123 | } | |
124 | ||
125 | if (out_pipe[0] != -1) { | |
126 | close(out_pipe[0]); | |
127 | } | |
128 | ||
129 | if (pid != 0) { | |
130 | if (waitpid(pid, &exit_code, 0) < 0) { | |
131 | fail("Could not wait for PID %d (dylib %s): err %s\n", pid, dylib, strerror(errno)); | |
132 | } | |
133 | ||
134 | if (!WIFEXITED(exit_code)) { | |
135 | fail("PID %d (%s) did not exit: %d. stdout: %s\n stderr: %s\n", pid, dylib, exit_code, child_stdout.c_str(), child_stderr.c_str()); | |
136 | } | |
137 | if (WEXITSTATUS(exit_code) != 0) { | |
138 | fail("Failed to validate dylib %s\nstdout: %s\nstderr: %s\n", dylib, child_stdout.c_str(), child_stderr.c_str()); | |
139 | } | |
140 | } | |
141 | ||
142 | testprintf("%s", child_stdout.c_str()); | |
143 | ||
144 | return 0; | |
145 | } | |
146 | ||
147 | bool check_class(Class cls, unsigned & cacheCount) { | |
148 | // printf("%s %s\n", class_getName(cls), class_isMetaClass(cls) ? "(metaclass)" : ""); | |
149 | ||
150 | // For the initialization of the cache so that we setup the constant cache if any | |
151 | class_getMethodImplementation(cls, @selector(initialize)); | |
152 | ||
153 | if (objc_cache_isConstantOptimizedCache(&(cls->cache), true, (uintptr_t)&_objc_empty_cache)) { | |
154 | cacheCount++; | |
155 | // printf("%s has a preopt cache\n", class_getName(cls)); | |
156 | ||
157 | // Make the union of all selectors until the preopt fallback class | |
158 | const class_ro_t * fallback = ((const objc_class *) objc_cache_preoptFallbackClass(&(cls->cache)))->data()->ro(); | |
159 | ||
160 | std::unordered_map<SEL, IMP> methods; | |
161 | ||
162 | Method *methodList; | |
163 | unsigned count; | |
164 | Class currentClass = cls; | |
165 | unsigned dynamicCount = 0; | |
166 | while (currentClass->data()->ro() != fallback) { | |
167 | methodList = class_copyMethodList(currentClass, &count); | |
168 | // printf("%d methods in method list for %s\n", count, class_getName(currentClass)); | |
169 | for (unsigned i = 0 ; i < count ; i++) { | |
170 | SEL sel = method_getName(methodList[i]); | |
171 | if (methods.find(sel) == methods.end()) { | |
172 | const char *name = sel_getName(sel); | |
173 | // printf("[dynamic] %s -> %p\n", name, method_getImplementation(methodList[i])); | |
174 | methods[sel] = ptrauth_strip(method_getImplementation(methodList[i]), ptrauth_key_function_pointer); | |
175 | if ( (currentClass == cls) || | |
176 | ( (strcmp(name, ".cxx_construct") != 0) | |
177 | && (strcmp(name, ".cxx_destruct") != 0))) { | |
178 | dynamicCount++; | |
179 | } | |
180 | } | |
181 | } | |
182 | if (count > 0) { | |
183 | free(methodList); | |
184 | } | |
185 | currentClass = class_getSuperclass(currentClass); | |
186 | } | |
187 | ||
188 | // Check we have an equality between the two caches | |
189 | ||
190 | // Count the methods in the preopt cache | |
191 | unsigned preoptCacheCount = 0; | |
192 | unsigned capacity = objc_cache_preoptCapacity(&(cls->cache)); | |
193 | const preopt_cache_entry_t *buckets = objc_cache_preoptCache(&(cls->cache))->entries; | |
194 | ||
195 | #pragma clang diagnostic push | |
196 | #pragma clang diagnostic ignored "-Wcast-of-sel-type" | |
197 | const uint8_t *selOffsetsBase = (const uint8_t*)@selector(🤯); | |
198 | #pragma clang diagnostic pop | |
199 | for (unsigned i = 0 ; i < capacity ; i++) { | |
200 | uint32_t selOffset = buckets[i].sel_offs; | |
201 | if (selOffset != 0xFFFFFFFF) { | |
202 | SEL sel = (SEL)(selOffsetsBase + selOffset); | |
203 | IMP imp = (IMP)((uint8_t*)cls - buckets[i].imp_offs); | |
204 | if (methods.find(sel) == methods.end()) { | |
205 | fail("ERROR: %s: %s not found in dynamic method list\n", class_getName(cls), sel_getName(sel)); | |
206 | return false; | |
207 | } | |
208 | IMP dynamicImp = methods.at(sel); | |
209 | // printf("[static] %s -> %p\n", sel_getName(sel), imp); | |
210 | if (imp != dynamicImp) { | |
211 | fail("ERROR: %s: %s has different implementations %p vs %p in static and dynamic caches", class_getName(cls), sel_getName(sel), imp, dynamicImp); | |
212 | return false; | |
213 | } | |
214 | preoptCacheCount++; | |
215 | } | |
216 | } | |
217 | ||
218 | if (preoptCacheCount != dynamicCount) { | |
219 | testwarn("Methods in preopt cache:\n"); | |
220 | ||
221 | for (unsigned i = 0 ; i < capacity ; i++) { | |
222 | uint32_t selOffset = buckets[i].sel_offs; | |
223 | if (selOffset != 0xFFFFFFFF) { | |
224 | SEL sel = (SEL)(selOffsetsBase + selOffset); | |
225 | testwarn("%s\n", sel_getName(sel)); | |
226 | } | |
227 | } | |
228 | ||
229 | testwarn("Methods in dynamic cache:\n"); | |
230 | ||
231 | for (const auto & [sel, imp] : methods) { | |
232 | testwarn("%s\n", sel_getName(sel)); | |
233 | } | |
234 | ||
235 | fail("ERROR: %s's preoptimized cache is missing some methods\n", class_getName(cls)); | |
236 | ||
237 | return false; | |
238 | } | |
239 | ||
240 | } else { | |
241 | // printf("%s does NOT have a preopt cache\n", class_getName(cls)); | |
242 | } | |
243 | ||
244 | return true; | |
245 | } | |
246 | ||
247 | bool check_library(const char *path) { | |
248 | std::set<std::string> blacklistedClasses { | |
249 | "PNPWizardScratchpadInkView", // Can only be +initialized on Pencil-capable devices | |
250 | "CACDisplayManager", // rdar://64929282 (CACDisplayManager does layout in +initialize!) | |
251 | }; | |
252 | ||
253 | testprintf("Checking %s… ", path); | |
254 | ||
255 | __unused void *lib = dlopen(path, RTLD_NOW); | |
256 | extern uint32_t _dyld_image_count(void) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0); | |
257 | unsigned outCount = 0; | |
258 | ||
259 | // Realize all classes first. | |
260 | Class *allClasses = objc_copyClassList(&outCount); | |
261 | if (allClasses != NULL) { | |
262 | free(allClasses); | |
263 | } | |
264 | ||
265 | allClasses = objc_copyClassesForImage(path, &outCount); | |
266 | if (allClasses != NULL) { | |
267 | unsigned classCount = 0; | |
268 | unsigned cacheCount = 0; | |
269 | ||
270 | for (const Class * clsPtr = allClasses ; *clsPtr != nil ; clsPtr++) { | |
271 | classCount++; | |
272 | Class cls = *clsPtr; | |
273 | ||
274 | if (blacklistedClasses.find(class_getName(cls)) != blacklistedClasses.end()) { | |
275 | continue; | |
276 | } | |
277 | ||
278 | if (!check_class(cls, cacheCount)) { | |
279 | return false; | |
280 | } | |
281 | ||
282 | if (!class_isMetaClass(cls)) { | |
283 | if (!check_class(object_getClass(cls), cacheCount)) { | |
284 | return false; | |
285 | } | |
286 | } | |
287 | } | |
288 | testprintf("checked %d caches in %d classes\n", cacheCount, classCount); | |
289 | free(allClasses); | |
290 | } else { | |
291 | testprintf("could not find %s or no class names inside\n", path); | |
292 | } | |
293 | ||
294 | return true; | |
295 | } | |
296 | ||
297 | size_t size_of_shared_cache_with_uuid(uuid_t uuid) { | |
298 | DIR* dfd = opendir(IPHONE_DYLD_SHARED_CACHE_DIR); | |
299 | if (!dfd) { | |
300 | fail("Error: unable to open shared cache dir %s\n", | |
301 | IPHONE_DYLD_SHARED_CACHE_DIR); | |
302 | exit(1); | |
303 | } | |
304 | ||
305 | uint64_t shared_cache_size = 0; | |
306 | ||
307 | struct dirent *dp; | |
308 | while ((dp = readdir(dfd))) { | |
309 | char full_filename[512]; | |
310 | snprintf(full_filename, sizeof(full_filename), "%s%s", | |
311 | IPHONE_DYLD_SHARED_CACHE_DIR, dp->d_name); | |
312 | ||
313 | struct stat stat_buf; | |
314 | if (stat(full_filename, &stat_buf) != 0) | |
315 | continue; | |
316 | ||
317 | if ((stat_buf.st_mode & S_IFMT) == S_IFDIR) | |
318 | continue; | |
319 | ||
320 | int fd = open(full_filename, O_RDONLY); | |
321 | if (fd < 0) { | |
322 | fprintf(stderr, "Error: unable to open file %s\n", full_filename); | |
323 | continue; | |
324 | } | |
325 | ||
326 | struct dyld_cache_header header; | |
327 | if (read(fd, &header, sizeof(header)) != sizeof(header)) { | |
328 | fprintf(stderr, "Error: unable to read dyld shared cache header from %s\n", | |
329 | full_filename); | |
330 | close(fd); | |
331 | continue; | |
332 | } | |
333 | ||
334 | if (uuid_compare(header.uuid, uuid) == 0) { | |
335 | shared_cache_size = stat_buf.st_size; | |
336 | break; | |
337 | } | |
338 | } | |
339 | ||
340 | closedir(dfd); | |
341 | ||
342 | return shared_cache_size; | |
343 | } | |
344 | ||
345 | int main (int argc, const char * argv[]) | |
346 | { | |
347 | if (argc == 1) { | |
348 | int err = 0; | |
349 | dyld_process_info process_info = _dyld_process_info_create(mach_task_self(), 0, &err); | |
350 | if (NULL == process_info) { | |
351 | mach_error("_dyld_process_info_create", err); | |
352 | fail("_dyld_process_info_create"); | |
353 | return 2; | |
354 | } | |
355 | dyld_process_cache_info cache_info; | |
356 | _dyld_process_info_get_cache(process_info, &cache_info); | |
357 | ||
358 | __block std::set<std::string> dylibsSet; | |
359 | size_t size = size_of_shared_cache_with_uuid(cache_info.cacheUUID); | |
360 | dyld_shared_cache_iterate((void*)cache_info.cacheBaseAddress, (uint32_t)size, ^(const dyld_shared_cache_dylib_info* dylibInfo, __unused const dyld_shared_cache_segment_info* segInfo) { | |
361 | if (dylibInfo->isAlias) return; | |
362 | std::string path(dylibInfo->path); | |
363 | dylibsSet.insert(path); | |
364 | }); | |
365 | std::vector<std::string> dylibs(dylibsSet.begin(), dylibsSet.end()); | |
366 | ||
367 | dispatch_apply(dylibs.size(), DISPATCH_APPLY_AUTO, ^(size_t idx) { | |
368 | validate_dylib_in_forked_process(argv[0], dylibs[idx].c_str()); | |
369 | }); | |
370 | } else { | |
371 | const char *libraryName = argv[1]; | |
372 | if (!check_library(libraryName)) { | |
373 | fail("checking library %s\n", libraryName); | |
374 | return 1; | |
375 | } | |
376 | } | |
377 | ||
378 | succeed(__FILE__); | |
379 | return 0; | |
380 | } |