rf-web/vendor/bundle/gems/rb-fsevent-0.10.3/ext/fsevent_watch/FSEventsFix.c
2019-10-21 10:18:17 +02:00

627 lines
22 KiB
C

/*
* 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 <andrey@tarantsov.com>
* Copyright (c) 2003 Constantin S. Svintsoff <kostik@iclub.nsu.ru>
*
* 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 <kostik@iclub.nsu.ru>
*
* 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 <dispatch/dispatch.h>
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <dlfcn.h>
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 <stdlib.h>
#import <string.h>
#import <sys/types.h>
#import <mach-o/dyld.h>
#import <mach-o/loader.h>
#import <mach-o/nlist.h>
#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();
}
}