/* * FSEventsFix * * Resolves a long-standing bug in realpath() that prevents FSEvents API from * monitoring certain folders on a wide range of OS X released (10.6-10.10 at least). * * The underlying issue is that for some folders, realpath() call starts returning * a path with incorrect casing (e.g. "/users/smt" instead of "/Users/smt"). * FSEvents is case-sensitive and calls realpath() on the paths you pass in, so * an incorrect value returned by realpath() prevents FSEvents from seeing any * change events. * * See the discussion at https://github.com/thibaudgg/rb-fsevent/issues/10 about * the history of this bug and how this library came to exist. * * This library uses Facebook's fishhook to replace a custom implementation of * realpath in place of the system realpath; FSEvents will then invoke our custom * implementation (which does not screw up the names) and will thus work correctly. * * Our implementation of realpath is based on the open-source implementation from * OS X 10.10, with a single change applied (enclosed in "BEGIN WORKAROUND FOR * OS X BUG" ... "END WORKAROUND FOR OS X BUG"). * * Include FSEventsFix.{h,c} into your project and call FSEventsFixInstall(). * * It is recommended that you install FSEventsFix on demand, using FSEventsFixIsBroken * to check if the folder you're about to pass to FSEventStreamCreate needs the fix. * Note that the fix must be applied before calling FSEventStreamCreate. * * FSEventsFixIsBroken requires a path that uses the correct case for all folder names, * i.e. a path provided by the system APIs or constructed from folder names provided * by the directory enumeration APIs. * * Copyright (c) 2015 Andrey Tarantsov * Copyright (c) 2003 Constantin S. Svintsoff * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * Based on a realpath implementation from Apple libc 498.1.7, taken from * http://www.opensource.apple.com/source/Libc/Libc-498.1.7/stdlib/FreeBSD/realpath.c * and provided under the following license: * * Copyright (c) 2003 Constantin S. Svintsoff * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The names of the authors may not be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include "FSEventsFix.h" #include #include #include #include #include #include #include #include #include #include const char *const FSEventsFixVersionString = "0.11.0"; #pragma mark - Forward declarations static char *(*orig_realpath)(const char *restrict file_name, char resolved_name[PATH_MAX]); static char *CFURL_realpath(const char *restrict file_name, char resolved_name[PATH_MAX]); static char *FSEventsFix_realpath_wrapper(const char *restrict src, char *restrict dst); static void _FSEventsFixHookInstall(); static void _FSEventsFixHookUninstall(); #pragma mark - Internal state static dispatch_queue_t g_queue = NULL; static int64_t g_enable_refcount = 0; static bool g_in_self_test = false; static bool g_hook_operational = false; static void(^g_logging_block)(FSEventsFixMessageType type, const char *message); static FSEventsFixDebugOptions g_debug_opt = 0; typedef struct { char *name; void *replacement; void *original; uint hooked_symbols; } rebinding_t; static rebinding_t g_rebindings[] = { { "_realpath$DARWIN_EXTSN", (void *) &FSEventsFix_realpath_wrapper, (void *) &realpath, 0 } }; static const uint g_rebindings_nel = sizeof(g_rebindings) / sizeof(g_rebindings[0]); #pragma mark - Logging static void _FSEventsFixLog(FSEventsFixMessageType type, const char *__restrict fmt, ...) __attribute__((__format__ (__printf__, 2, 3))); static void _FSEventsFixLog(FSEventsFixMessageType type, const char *__restrict fmt, ...) { if (g_logging_block) { char *message = NULL; va_list va; va_start(va, fmt); vasprintf(&message, fmt, va); va_end(va); if (message) { if (!!(g_debug_opt & FSEventsFixDebugOptionLogToStderr)) { fprintf(stderr, "FSEventsFix: %s\n", message); } if (g_logging_block) { g_logging_block(type, message); } free(message); } } } #pragma mark - API void _FSEventsFixInitialize() { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ g_queue = dispatch_queue_create("FSEventsFix", DISPATCH_QUEUE_SERIAL); }); } void FSEventsFixConfigure(FSEventsFixDebugOptions debugOptions, void(^loggingBlock)(FSEventsFixMessageType severity, const char *message)) { _FSEventsFixInitialize(); loggingBlock = Block_copy(loggingBlock); dispatch_sync(g_queue, ^{ g_debug_opt = debugOptions; g_logging_block = loggingBlock; }); } // Must be called from the private serial queue. void _FSEventsFixSelfTest() { g_in_self_test = true; g_hook_operational = false; static char result[1024]; realpath("/Etc/__!FSEventsFixSelfTest!__", result); g_in_self_test = false; } void FSEventsFixEnable() { _FSEventsFixInitialize(); dispatch_sync(g_queue, ^{ if (++g_enable_refcount == 1) { orig_realpath = dlsym(RTLD_DEFAULT, "realpath"); _FSEventsFixHookInstall(); _FSEventsFixSelfTest(); if (g_hook_operational) { _FSEventsFixLog(FSEventsFixMessageTypeStatusChange, "Enabled"); } else { _FSEventsFixLog(FSEventsFixMessageTypeFatalError, "Failed to enable (hook not called)"); } } }); } void FSEventsFixDisable() { _FSEventsFixInitialize(); dispatch_sync(g_queue, ^{ if (g_enable_refcount == 0) { abort(); } if (--g_enable_refcount == 0) { _FSEventsFixHookUninstall(); _FSEventsFixSelfTest(); if (!g_hook_operational) { _FSEventsFixLog(FSEventsFixMessageTypeStatusChange, "Disabled"); } else { _FSEventsFixLog(FSEventsFixMessageTypeFatalError, "Failed to disable (hook still called)"); } } }); } bool FSEventsFixIsOperational() { _FSEventsFixInitialize(); __block bool result = false; dispatch_sync(g_queue, ^{ result = g_hook_operational; }); return result; } bool _FSEventsFixIsBroken_noresolve(const char *resolved) { if (!!(g_debug_opt & FSEventsFixDebugOptionSimulateBroken)) { if (strstr(resolved, FSEventsFixSimulatedBrokenFolderMarker)) { return true; } } char *reresolved = realpath(resolved, NULL); if (reresolved) { bool broken = (0 != strcmp(resolved, reresolved)); free(reresolved); return broken; } else { return true; } } bool FSEventsFixIsBroken(const char *path) { char *resolved = CFURL_realpath(path, NULL); if (!resolved) { return true; } bool broken = _FSEventsFixIsBroken_noresolve(resolved); free(resolved); return broken; } char *FSEventsFixCopyRootBrokenFolderPath(const char *inpath) { if (!FSEventsFixIsBroken(inpath)) { return NULL; } // get a mutable copy of an absolute path char *path = CFURL_realpath(inpath, NULL); if (!path) { return NULL; } for (;;) { char *sep = strrchr(path, '/'); if ((sep == NULL) || (sep == path)) { break; } *sep = 0; if (!_FSEventsFixIsBroken_noresolve(path)) { *sep = '/'; break; } } return path; } static void _FSEventsFixAttemptRepair(const char *folder) { int rv = rename(folder, folder); if (!!(g_debug_opt & FSEventsFixDebugOptionSimulateRepair)) { const char *pos = strstr(folder, FSEventsFixSimulatedBrokenFolderMarker); if (pos) { char *fixed = strdup(folder); fixed[pos - folder] = 0; strcat(fixed, pos + strlen(FSEventsFixSimulatedBrokenFolderMarker)); rv = rename(folder, fixed); free(fixed); } } if (rv != 0) { if (errno == EPERM) { _FSEventsFixLog(FSEventsFixMessageTypeResult, "Permission error when trying to repair '%s'", folder); } else { _FSEventsFixLog(FSEventsFixMessageTypeExpectedFailure, "Unknown error when trying to repair '%s': errno = %d", folder, errno); } } } FSEventsFixRepairStatus FSEventsFixRepairIfNeeded(const char *inpath) { char *root = FSEventsFixCopyRootBrokenFolderPath(inpath); if (root == NULL) { return FSEventsFixRepairStatusNotBroken; } for (;;) { _FSEventsFixAttemptRepair(root); char *newRoot = FSEventsFixCopyRootBrokenFolderPath(inpath); if (newRoot == NULL) { _FSEventsFixLog(FSEventsFixMessageTypeResult, "Repaired '%s' in '%s'", root, inpath); free(root); return FSEventsFixRepairStatusRepaired; } if (0 == strcmp(root, newRoot)) { _FSEventsFixLog(FSEventsFixMessageTypeResult, "Failed to repair '%s' in '%s'", root, inpath); free(root); free(newRoot); return FSEventsFixRepairStatusFailed; } _FSEventsFixLog(FSEventsFixMessageTypeResult, "Partial success, repaired '%s' in '%s'", root, inpath); free(root); root = newRoot; } } #pragma mark - FSEventsFix realpath wrapper static char *FSEventsFix_realpath_wrapper(const char * __restrict src, char * __restrict dst) { if (g_in_self_test) { if (strstr(src, "__!FSEventsFixSelfTest!__")) { g_hook_operational = true; } } // CFURL_realpath doesn't support putting where resolution failed into the // dst buffer, so we call the original realpath here first and if it gets a // result, replace that with the output of CFURL_realpath. that way all the // features of the original realpath are available. char *rv = NULL; char *orv = orig_realpath(src, dst); if (orv != NULL) { rv = CFURL_realpath(src, dst); } if (!!(g_debug_opt & FSEventsFixDebugOptionLogCalls)) { char *result = rv ?: dst; _FSEventsFixLog(FSEventsFixMessageTypeCall, "realpath(%s) => %s\n", src, result); } if (!!(g_debug_opt & FSEventsFixDebugOptionUppercaseReturn)) { char *result = rv ?: dst; if (result) { for (char *pch = result; *pch; ++pch) { *pch = (char)toupper(*pch); } } } return rv; } #pragma mark - realpath // naive implementation of realpath on top of CFURL // NOTE: doesn't quite support the full range of errno results one would // expect here, in part because some of these functions just return a boolean, // and in part because i'm not dealing with messy CFErrorRef objects and // attempting to translate those to sane errno values. // NOTE: the OSX realpath will return _where_ resolution failed in resolved_name // if passed in and return NULL. we can't properly support that extension here // since the resolution happens entirely behind the scenes to us in CFURL. static char* CFURL_realpath(const char *file_name, char resolved_name[PATH_MAX]) { char* resolved; CFURLRef url1; CFURLRef url2; CFStringRef path; if (file_name == NULL) { errno = EINVAL; return (NULL); } #if __DARWIN_UNIX03 if (*file_name == 0) { errno = ENOENT; return (NULL); } #endif // create a buffer to store our result if we weren't passed one if (!resolved_name) { if ((resolved = malloc(PATH_MAX)) == NULL) return (NULL); } else { resolved = resolved_name; } url1 = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8*)file_name, (CFIndex)strlen(file_name), false); if (url1 == NULL) { goto error_return; } url2 = CFURLCopyAbsoluteURL(url1); CFRelease(url1); if (url2 == NULL) { goto error_return; } url1 = CFURLCreateFileReferenceURL(NULL, url2, NULL); CFRelease(url2); if (url1 == NULL) { goto error_return; } // if there are multiple hard links to the original path, this may end up // being _completely_ different from what was intended url2 = CFURLCreateFilePathURL(NULL, url1, NULL); CFRelease(url1); if (url2 == NULL) { goto error_return; } path = CFURLCopyFileSystemPath(url2, kCFURLPOSIXPathStyle); CFRelease(url2); if (path == NULL) { goto error_return; } bool success = CFStringGetCString(path, resolved, PATH_MAX, kCFStringEncodingUTF8); CFRelease(path); if (!success) { goto error_return; } return resolved; error_return: if (!resolved_name) { // we weren't passed in an output buffer and created our own. free it int e = errno; free(resolved); errno = e; } return (NULL); } #pragma mark - fishhook // Copyright (c) 2013, Facebook, Inc. // All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name Facebook nor the names of its contributors may be used to // endorse or promote products derived from this software without specific // prior written permission. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #import #import #import #import #import #import #ifdef __LP64__ typedef struct mach_header_64 mach_header_t; typedef struct segment_command_64 segment_command_t; typedef struct section_64 section_t; typedef struct nlist_64 nlist_t; #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64 #else typedef struct mach_header mach_header_t; typedef struct segment_command segment_command_t; typedef struct section section_t; typedef struct nlist nlist_t; #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT #endif static volatile bool g_hook_installed = false; static void _FSEventsFixHookUpdateSection(section_t *section, intptr_t slide, nlist_t *symtab, char *strtab, uint32_t *indirect_symtab) { uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); for (uint i = 0; i < section->size / sizeof(void *); i++) { uint32_t symtab_index = indirect_symbol_indices[i]; if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) { continue; } uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; char *symbol_name = strtab + strtab_offset; for (rebinding_t *cur = g_rebindings, *end = g_rebindings + g_rebindings_nel; cur < end; ++cur) { if (strcmp(symbol_name, cur->name) == 0) { if (g_hook_installed) { if (indirect_symbol_bindings[i] != cur->replacement) { indirect_symbol_bindings[i] = cur->replacement; ++cur->hooked_symbols; } } else if (cur->original != NULL) { if (indirect_symbol_bindings[i] == cur->replacement) { indirect_symbol_bindings[i] = cur->original; if (cur->hooked_symbols > 0) { --cur->hooked_symbols; } } } goto symbol_loop; } } symbol_loop:; } } static void _FSEventsFixHookUpdateImage(const struct mach_header *header, intptr_t slide) { Dl_info info; if (dladdr(header, &info) == 0) { return; } segment_command_t *cur_seg_cmd; segment_command_t *linkedit_segment = NULL; struct symtab_command* symtab_cmd = NULL; struct dysymtab_command* dysymtab_cmd = NULL; uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t); for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { cur_seg_cmd = (segment_command_t *)cur; if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { linkedit_segment = cur_seg_cmd; } } else if (cur_seg_cmd->cmd == LC_SYMTAB) { symtab_cmd = (struct symtab_command*)cur_seg_cmd; } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; } } if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment || !dysymtab_cmd->nindirectsyms) { return; } // Find base symbol/string table addresses uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); // Get indirect symbol table (array of uint32_t indices into symbol table) uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff); cur = (uintptr_t)header + sizeof(mach_header_t); for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { cur_seg_cmd = (segment_command_t *)cur; if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0) { continue; } for (uint j = 0; j < cur_seg_cmd->nsects; j++) { section_t *sect = (section_t *)(cur + sizeof(segment_command_t)) + j; if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) { _FSEventsFixHookUpdateSection(sect, slide, symtab, strtab, indirect_symtab); } if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) { _FSEventsFixHookUpdateSection(sect, slide, symtab, strtab, indirect_symtab); } } } } } static void _FSEventsFixHookSaveOriginals() { for (rebinding_t *cur = g_rebindings, *end = g_rebindings + g_rebindings_nel; cur < end; ++cur) { void *original = cur->original = dlsym(RTLD_DEFAULT, cur->name+1); if (!original) { const char *error = dlerror(); _FSEventsFixLog(FSEventsFixMessageTypeFatalError, "Cannot find symbol %s, dlsym says: %s\n", cur->name, error); } } } static void _FSEventsFixHookUpdate() { uint32_t c = _dyld_image_count(); for (uint32_t i = 0; i < c; i++) { _FSEventsFixHookUpdateImage(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); } } static void _FSEventsFixHookInstall() { static bool first_rebinding_done = false; if (!g_hook_installed) { g_hook_installed = true; if (!first_rebinding_done) { first_rebinding_done = true; _FSEventsFixHookSaveOriginals(); _dyld_register_func_for_add_image(_FSEventsFixHookUpdateImage); } else { _FSEventsFixHookUpdate(); } } } static void _FSEventsFixHookUninstall() { if (g_hook_installed) { g_hook_installed = false; _FSEventsFixHookUpdate(); } }