/* * FILE: watchvol.c * AUTH: Soren Spies (sspies) * DATE: 5 March 2006 * DESC: watch volumes as they come, go, or are changed, fire rebuilds * NOTE: everything in this file should happen on the main thread * * Here's roughly how it all works. * 1. sign up for diskarb notifications * 2. generate a data structure for each incoming comprehensible OS volume * 2a. set up notifications for all relevant paths on said volume * [notifications <-> structures] * (2) uses bootcaches.plist to describe what caches a system needs. * All top-level keys are assumed required (which means the mkext could * get fancier in the future if an old-fashioned mkext was still okay). * If keys exist that can't be understood or don't parse correctly, * we bail on watching that volume. * * 3. intelligently respond to notifications * 3a. set up a timer to fire so the system has time to settle * 3b. upon lazy firing, rebuild caches OR copy files to Apple_Boot * 3c. if someone tries to unmount a volume, cancel any timer and check * 3d. if a locker unlocks happily, cancel any timer and check (TODO) * 3e. if a locker unlocks unhappily, need to force a check of non-caches? (???) * 3f. we don't care if the volume is locked; additional kextcaches wait * (3d) has the effect that the first kextcache effectively triggers the * second one which copies caches down. It also allows us ... to be * smart about things like forcing reboots if we booted from staleness. * * 4. arbitrate kextcache locks * 4a. keep a Mach send right to a receive right in the locker * 4b. detect crashes via CFMachPortInvalidaton callback * 4c. take success information on unlock * 4d. if a lock was lost, force a rebuild (XX)? * TODO (still as part of 4252674): * (4) means that kextcache rebuilds the mkext (-> scheduling a timer) * (4c) means that we can schedule the Apple_Boot check on success (unschedules) * * 5. keep structures up to date * 5a. clean up when a volume goes away * 5b. disappear/appear whenever there's a change * * 6. reboot stuff: take a big lock; free it only if locker dies * * given that we read bootcaches.plist, we don't trust anything in it * ... but we push the checking off to kextcache, which ensures * (via dev_t/safecalls) that it is only operating on a single volume and * not being redirected to other volumes, etc. We have had Security review. * * XX we need to figure out what to do about ignored owners and UID 99. * We don't want to go writing an mkext with uid 99 ... but we do want * this scheme to work ... could we temporarily enable owners?? * The current plan (4554031) is to notice when VQ_UPDATE's occur ... * enabling owners ourselves could lead to some very mysterious behavior. * * $removing checkin comments expander in the header$ */ // system includes #include // yay notify_monitor_file (well, maybe someday ;) #include // strerror() #include #include #include #include // waitpid() (fork/daemon/waitpid) #include // " " #include // daemon(3) #include // e.g. execv #include #include #include #include // notifyd SPI :P (at least the latter two are API for Leopard) extern uint32_t notify_monitor_file(int token, const char *name, int flags); #ifdef MAC_OS_X_VERSION_10_5 extern uint32_t notify_set_state(int token, uint64_t state); extern uint32_t notify_get_state(int token, uint64_t *state); #else extern uint32_t notify_set_state(int token, int state); extern uint32_t notify_get_state(int token, int *state); #endif // project includes #include "watchvol.h" // kextd_watch_volumes #include "bootroot.h" // watchedVol #include "logging.h" #include "globals.h" // gClientUID // #include "safecalls.h" // constants #define kWatchKeyBase "com.apple.system.kextd.fswatch" #define kWatchSettleTime 5 // struct watchedVol's (struct bootCaches in bootroot.h) // created/destroyed with volumes coming/going; stored in sFsysWatchDict // use notify_set_state on our notifications to point to these objects struct watchedVol { // CFStringRef bsdName; // DA id (is the key in sFsysWatchDict) CFRunLoopTimerRef delayer; // non-NULL if something is scheduled CFMachPortRef lock; // send right to locker's port int errcount; // did most recent locker report an error? (???) Boolean disableOwners; // should we disable owners on unlock? CFMutableArrayRef tokens; // notify(3) tokens struct bootCaches *caches; // parsed version of bootcaches.plist }; // module-wide data static DASessionRef sDASession = NULL; // learn about volumes static DAApprovalSessionRef sDAApproval = NULL; // retain volumes static CFMachPortRef sFsysChangedPort = NULL; // let us know static CFRunLoopSourceRef sFsysChangedSource = NULL; // on the runloop static CFMutableDictionaryRef sFsysWatchDict = NULL; // disk ids -> wstruct*s static CFMachPortRef sRebootLock = NULL; // if locked for reboot // function declarations (kextd_watch_volumes, _stop in watchvol.h) // ctor/dtor for struct watchedVol static struct watchedVol* create_watchedVol(CFURLRef volURL); static void destroy_watchedVol(struct watchedVol *watched); // volume state static void vol_appeared(DADiskRef disk, void *ctx); static void vol_changed(DADiskRef, CFArrayRef keys, void* ctx); static void vol_disappeared(DADiskRef, void* ctx); static DADissenterRef is_dadisk_busy(DADiskRef, void *ctx); static Boolean check_vol_busy(struct watchedVol *watched); // notification processing delay scheme static void fsys_changed(CFMachPortRef p, void *msg, CFIndex size, void *info); static void check_now(CFRunLoopTimerRef timer, void *ctx); // notify timer cb // actual rebuilders! static Boolean check_rebuild(struct watchedVol*, Boolean force); // do anything? // CFMachPort invalidation callback static void lock_died(CFMachPortRef p, void *info); // XX reboot checks (also defined in MiG) and helpers static Boolean reconsiderVolumes(dev_path_t busyDev); static Boolean reconsiderVolume(dev_path_t volDev); static void toggleOwners(dev_path_t disk, Boolean enableOwners); // additional "local" helpers are declared/defined just before use // utility macros #define CFRELEASE(x) if(x) { CFRelease(x); x = NULL; } /****************************************************************************** * kextd_watch_volumes sets everything up (on the current runloop) *****************************************************************************/ #if 0 // for testing #define twrite(msg) write(STDERR_FILENO, msg, sizeof(msg)) static void debug_chld(int signum) __attribute__((unused)) { int olderrno = errno; int status; pid_t childpid; if (signum != SIGCHLD) twrite("debug_chld not registered for signal\n"); else if ((childpid = waitpid(-1, &status, WNOHANG)) == -1) twrite("DEBUG: SIGCHLD received, but no children available?\n"); else if (!WIFEXITED(status)) twrite("DEBUG: child quit on signal?\n"); else if (WEXITSTATUS(status)) twrite("DEBUG: child exited with unhappy status\n"); else twrite("DEBUG: child exited with happy status\n"); errno = olderrno; } #endif int kextd_watch_volumes(int sourcePriority) { int rval = ELAST + 1; char *errmsg; CFRunLoopRef rl; errmsg = "already watching volumes?!"; if (sFsysWatchDict) goto finish; // the callbacks will want to go digging in here, so set it up first errmsg = "couldn't create data structures"; sFsysWatchDict = CFDictionaryCreateMutable(nil, 0, &kCFTypeDictionaryKeyCallBacks, NULL); // storing watchedVol*'s if (!sFsysWatchDict) goto finish; errmsg = "trouble setting up ports and sources"; rl = CFRunLoopGetCurrent(); if (!rl) goto finish; // change notifications will eventually come in through this port/source sFsysChangedPort = CFMachPortCreate(nil, fsys_changed, NULL, NULL); // we have to keep these objects so we can unschedule them later? if (!sFsysChangedPort) goto finish; sFsysChangedSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, sFsysChangedPort, sourcePriority); if (!sFsysChangedSource) goto finish; CFRunLoopAddSource(rl, sFsysChangedSource, kCFRunLoopDefaultMode); // in general, being on the runloop means we could be called ... // and we are thus careful about our ordering. In practice, however, // we're adding to the current runloop, which means nothing can happen // until this routine exits (we're on the one and only thread). /* * XX need to set up a better match dictionary * kDADiskDescriptionMediaWritableKey = true * kDADiskDescriptionVolumeNetworkKey != true */ // make sure we have a chance to block unmounts errmsg = "couldn't set up diskarb sessions"; sDAApproval = DAApprovalSessionCreate(nil); if (!sDAApproval) goto finish; DARegisterDiskUnmountApprovalCallback(sDAApproval, kDADiskDescriptionMatchVolumeMountable, is_dadisk_busy, NULL); DAApprovalSessionScheduleWithRunLoop(sDAApproval, rl,kCFRunLoopDefaultMode); // set up the regular session sDASession = DASessionCreate(nil); if (!sDASession) goto finish; DARegisterDiskAppearedCallback(sDASession, kDADiskDescriptionMatchVolumeMountable, vol_appeared, NULL); DARegisterDiskDescriptionChangedCallback(sDASession, kDADiskDescriptionMatchVolumeMountable, kDADiskDescriptionWatchVolumePath, vol_changed, NULL); DARegisterDiskDisappearedCallback(sDASession, kDADiskDescriptionMatchVolumeMountable, vol_disappeared, NULL); // okay, we're ready to rumble! DASessionScheduleWithRunLoop(sDASession, rl, kCFRunLoopDefaultMode); // if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) goto finish; // errmsg = "couldn't set debug signal handler"; // if (signal(SIGCHLD, debug_chld) == SIG_ERR) goto finish; errmsg = NULL; rval = 0; // volume notifications should start coming in shortly finish: if (rval) { kextd_error_log("kextd_watch_volumes: %s", errmsg); kextd_stop_volwatch(); } return rval; } /****************************************************************************** * kextd_giveup_volwatch (4692369) simply initializes sFsysWatchDict to * be empty so that reboot locking can occur. *****************************************************************************/ int kextd_giveup_volwatch() { int rval = ENOMEM; sFsysWatchDict = CFDictionaryCreateMutable(nil, 0, &kCFTypeDictionaryKeyCallBacks, NULL); // storing watchedVol*'s if (sFsysWatchDict) { rval = 0; } else { kextd_error_log("giveup_volwatch(): allocation failure"); } return rval; } /****************************************************************************** * kextd_stop_volwatch unregisters from everything and cleans up * - called from watch_volumes to handle partial cleanup *****************************************************************************/ // to help clear out sFsysWatch static void free_dict_item(const void* key, const void *val, void *c) { destroy_watchedVol((struct watchedVol*)val); } // public entry point to this module void kextd_stop_volwatch() { CFRunLoopRef rl; // runloop cleanup rl = CFRunLoopGetCurrent(); if (rl && sDASession) DASessionUnscheduleFromRunLoop(sDASession, rl, kCFRunLoopDefaultMode); if (rl && sDAApproval) DAApprovalSessionUnscheduleFromRunLoop(sDAApproval, rl, kCFRunLoopDefaultMode); // use CFRELEASE to nullify cfrefs in case watch_volumes called again if (sDASession) { DAUnregisterCallback(sDASession, vol_disappeared, NULL); DAUnregisterCallback(sDASession, vol_changed, NULL); DAUnregisterCallback(sDASession, vol_appeared, NULL); CFRELEASE(sDASession); } if (sDAApproval) { DAUnregisterApprovalCallback(sDAApproval, is_dadisk_busy, NULL); CFRELEASE(sDAApproval); } if (rl && sFsysChangedSource) CFRunLoopRemoveSource(rl, sFsysChangedSource, kCFRunLoopDefaultMode); CFRELEASE(sFsysChangedSource); CFRELEASE(sFsysChangedPort); if (sFsysWatchDict) { CFDictionaryApplyFunction(sFsysWatchDict, free_dict_item, NULL); CFRELEASE(sFsysWatchDict); } return; } /****************************************************************************** * destroy_watchedVol unregisters any notification tokens and frees * pieces created in create_watchedVol ******************************************************************************/ static void destroy_watchedVol(struct watchedVol *watched) { CFIndex ntokens; int token; // assert that ->delayer and ->lock have already been cleaned up if (watched->tokens) { ntokens = CFArrayGetCount(watched->tokens); while(ntokens--) { token = (int)CFArrayGetValueAtIndex(watched->tokens,ntokens); // XX should take (hacky) steps to insure token is never zero? if (/* !token || */ notify_cancel(token)) kextd_error_log("destroy_watchedVol: " "trouble canceling notification?"); } CFRelease(watched->tokens); } if (watched->caches) destroyCaches(watched->caches); } /****************************************************************************** * create_watchedVol calls readCaches and creates watch-specific necessities ******************************************************************************/ static struct watchedVol* create_watchedVol(CFURLRef volURL) { struct watchedVol *watched, *rval = NULL; char *errmsg = NULL; char rootpath[PATH_MAX] = { '\0' }; Boolean isGPT = false; char *bsdname; Boolean ownersIgnored = false; struct statfs sfs; errmsg = "allocation error"; watched = calloc(1, sizeof(*watched)); if (!watched) goto finish; if (!CFURLGetFileSystemRepresentation(volURL, false, /*CF folk: make up your minds!*/ (UInt8*)rootpath, PATH_MAX)) goto finish; // 4616366 only watch BootRoot volumes if (!isBootRoot(rootpath, &isGPT) || !isGPT) { errmsg = NULL; goto finish; } // There will be RPS paths, booters, "misc" paths, and the exts folder. // For now, we'll just set the array size to 0 and let it grow. watched->tokens = CFArrayCreateMutable(nil, 0, NULL); if (!watched->tokens) goto finish; // try to enable owners if currently ignored if (statfs(rootpath, &sfs)) goto finish; if ((bsdname = strstr(sfs.f_mntfromname, "disk"))) { ownersIgnored = ((sfs.f_flags & MNT_IGNORE_OWNERSHIP) != 0); if (ownersIgnored) toggleOwners(bsdname, true); } errmsg = NULL; // readCaches logs its own errors watched->caches = readCaches(rootpath); if (!watched->caches) goto finish; rval = watched; // success? finish: if (ownersIgnored) toggleOwners(bsdname, false); // toggle back if (errmsg) { if (rootpath[0]) { kextd_error_log("%s: %s", rootpath, errmsg); } else { kextd_error_log("create_watchedVol(): %s", errmsg); } } if (!rval && watched) { destroy_watchedVol(watched); } return rval; } // helper #define cleanupLock(/*CFMachPortRef*/ lock) \ { \ mach_port_t lport; \ \ if (lock) { \ lport = CFMachPortGetPort(lock); \ CFRelease(lock); \ lock = NULL; \ mach_port_deallocate(mach_task_self(), lport); \ } \ } /****************************************************************************** * vol_appeared checks whether a volume is interesting * (note: the first time we see a volume, it's probably not mounted yet) * (we rely on vol_changed to call us when the mountpoint actually appears) * - signs up for notifications -> creates new entries in our structures * - initiates an initial volume check *****************************************************************************/ // set up notifications for a single path static int watch_path(char *path, mach_port_t port, struct watchedVol* watched) { int rval = ELAST + 1; // cheesy char key[PATH_MAX]; int token = 0; #ifdef MAC_OS_X_VERSION_10_5 uint64_t state; #else int state; #endif // generate key, register for token, monitor, record pointer in token if (strlcpy(key, kWatchKeyBase, PATH_MAX) >= PATH_MAX) goto finish; if (strlcat(key, path, PATH_MAX) >= PATH_MAX) goto finish; if (notify_register_mach_port(key, &port, NOTIFY_REUSE, &token)) goto finish; state = (intptr_t)watched; if (notify_set_state(token, state)) goto finish; if (notify_monitor_file(token, path, 1)) goto finish; CFArrayAppendValue(watched->tokens, (void*)token); rval = 0; finish: if (rval && token != -1 && notify_cancel(token)) kextd_error_log("watch_path: trouble canceling token?"); return rval; } #define makerootpath(caches, dst, path) do { \ if (strlcpy(dst, caches->root, PATH_MAX) >= PATH_MAX) goto finish; \ if (strlcat(dst, path, PATH_MAX) >= PATH_MAX) goto finish; \ } while(0) static void vol_appeared(DADiskRef disk, void *ctx) { int result = 0; // for now, ignore inability to get basic data (4528851) mach_port_t fsPort; CFDictionaryRef ddesc = NULL; CFBooleanRef traitVal; CFURLRef volURL; CFStringRef bsdName; struct watchedVol *watched = NULL; struct bootCaches *caches; int i; char path[PATH_MAX]; // see if the disk is writable, etc ddesc = DADiskCopyDescription(disk); if (!ddesc) goto finish; // note: "whole" media (e.g. "disk1") have *either* mountpoints or children volURL = CFDictionaryGetValue(ddesc, kDADiskDescriptionVolumePathKey); if (!volURL || CFGetTypeID(volURL) != CFURLGetTypeID()) goto finish; // bsdName is the key in the dictionary (might we already be watching?) bsdName = CFDictionaryGetValue(ddesc, kDADiskDescriptionMediaBSDNameKey); if (!bsdName || CFGetTypeID(bsdName) != CFStringGetTypeID()) goto finish; if (CFDictionaryGetValue(sFsysWatchDict, bsdName)) { kextd_error_log("refreshing watch of volume already in watch table?"); vol_disappeared(disk, ctx); // brush before uncharted territory } // check traits (haven't seen writable network volumes; need to custom dict) traitVal = CFDictionaryGetValue(ddesc, kDADiskDescriptionMediaWritableKey); if (!traitVal || CFGetTypeID(traitVal) != CFBooleanGetTypeID()) goto finish; if (CFEqual(traitVal, kCFBooleanFalse)) goto finish; traitVal = CFDictionaryGetValue(ddesc, kDADiskDescriptionVolumeNetworkKey); if (!traitVal || CFGetTypeID(traitVal) != CFBooleanGetTypeID()) goto finish; if (CFEqual(traitVal, kCFBooleanTrue)) goto finish; // kextd_log("DEBUG: proceeding with volume:"); //CFShow(bsdName); // does it have a usable bootcaches.plist? (if not, just ignored) if (!(watched = create_watchedVol(volURL))) goto finish; result = -1; // anything after this is an error caches = watched->caches; // set up notifications on the change port fsPort = CFMachPortGetPort(sFsysChangedPort); if (fsPort == MACH_PORT_NULL) goto finish; // for path in { exts, rpspaths[], booters, miscpaths[] } // rpspaths contains mkext, bootconfig; miscpaths the label file // cache paths are relative; need to make absolute makerootpath(caches, path, caches->exts); if (watch_path(path, fsPort, watched)) goto finish; for (i = 0; i < caches->nrps; i++) { makerootpath(caches, path, caches->rpspaths[i].rpath); if (watch_path(path, fsPort, watched)) goto finish; } if (caches->efibooter.rpath[0]) { makerootpath(caches, path, caches->efibooter.rpath); if (watch_path(path, fsPort, watched)) goto finish; } if (caches->ofbooter.rpath[0]) { makerootpath(caches, path, caches->ofbooter.rpath); if (watch_path(path, fsPort, watched)) goto finish; } for (i = 0; i < caches->nmisc; i++) { makerootpath(caches, path, caches->miscpaths[i].rpath); if (watch_path(path, fsPort, watched)) goto finish; } // we cleaned up any pre-existing entry for bsdName above CFDictionarySetValue(sFsysWatchDict, bsdName, watched); (void)check_rebuild(watched, false); // in case it needs an update result = 0; // we made it :) finish: if (ddesc) CFRelease(ddesc); if (result) { if (watched) { kextd_error_log("trouble setting up notifications on %s", watched->caches->root); destroy_watchedVol(watched); } // else kextd_log("DEBUG: skipping uninteresting volume"); } return; } /****************************************************************************** * vol_changed updates our structures if the mountpoint changed * - includes the initial mount after a device appears * - thus we only call appeared and disappeared as appropriate * _appeared and _disappeared are smart enough, but debugging is a pain * when vol_disappeared gets called on a volume mount! *****************************************************************************/ static void vol_changed(DADiskRef disk, CFArrayRef keys, void* ctx) { CFIndex i = CFArrayGetCount(keys); CFTypeRef key; CFDictionaryRef ddesc = DADiskCopyDescription(disk); CFStringRef bsdName = NULL; if (!ddesc) goto finish; // can't do much otherwise bsdName = CFDictionaryGetValue(ddesc,kDADiskDescriptionMediaBSDNameKey); if (!bsdName) goto finish; while (i--) if ((key = CFArrayGetValueAtIndex(keys, i)) && CFEqual(key, kDADiskDescriptionVolumePathKey)) { // kextd_log("DEBUG: mountpoint changed"); // XX need to use a custom match dictionary // diskarb sends lots of notifications about random stuff // thus: only need to call _disappeared if we're watching it if (CFDictionaryGetValue(sFsysWatchDict, bsdName)) vol_disappeared(disk, ctx); // and: only need to call _appeared if there's a mountpoint if (CFDictionaryGetValue(ddesc, key)) vol_appeared(disk, ctx); } else { kextd_log("vol_changed: ignoring update: no mountpoint change"); } finish: if (ddesc) CFRelease(ddesc); } /****************************************************************************** * vol_disappeared removes entries from the relevant structures * - handles forced removal by invalidating the lock *****************************************************************************/ static void vol_disappeared(DADiskRef disk, void* ctx) { int result = 0; // ignore weird requests (4528851) CFDictionaryRef ddesc = NULL; CFStringRef bsdName; struct watchedVol *watched; ddesc = DADiskCopyDescription(disk); if (!ddesc) goto finish; bsdName = CFDictionaryGetValue(ddesc, kDADiskDescriptionMediaBSDNameKey); if (!bsdName || CFGetTypeID(bsdName) != CFStringGetTypeID()) goto finish; //kextd_log("DEBUG: vol_disappeared:"); //CFShow(bsdName); watched = (void*)CFDictionaryGetValue(sFsysWatchDict, bsdName); if (!watched) goto finish; CFDictionaryRemoveValue(sFsysWatchDict, bsdName); // and in case some action was in progress if (watched->delayer) { CFRunLoopTimerInvalidate(watched->delayer); // refcount->0 watched->delayer = NULL; } // disappeared means perm/noperm doesn't matter cleanupLock(watched->lock); destroy_watchedVol(watched); // cancels notifications result = 0; finish: if (result) kextd_error_log("vol_disappeared: unexpected error"); if (ddesc) CFRelease(ddesc); return; } /****************************************************************************** * is_dadisk_busy lets diskarb know if we'd rather nothing changed * note: dissenter callback is called when root initiates an unmount, * but the result is ignored. :) *****************************************************************************/ static DADissenterRef is_dadisk_busy(DADiskRef disk, void *ctx) { int result = 0; // ignore weird requests for now (4528851) DADissenterRef rval = NULL; CFDictionaryRef ddesc = NULL; CFStringRef bsdName = NULL; struct watchedVol *watched; // kextd_log("DEBUG: is_dadisk_busy called"); ddesc = DADiskCopyDescription(disk); if (!ddesc) goto finish; bsdName = CFDictionaryGetValue(ddesc, kDADiskDescriptionMediaBSDNameKey); if (!bsdName || CFGetTypeID(bsdName) != CFStringGetTypeID()) goto finish; result = -1; watched = (void*)CFDictionaryGetValue(sFsysWatchDict, bsdName); if (watched && check_vol_busy(watched)) { rval = DADissenterCreate(nil, kDAReturnBusy, CFSTR("kextmanager busy")); if (!rval) goto finish; } result = 0; finish: if (result) kextd_error_log("is_dadisk_busy had trouble answering diskarb"); // else kextd_log("returning dissenter %p", rval); if (ddesc) CFRelease(ddesc); return rval; // caller releases dissenter if non-null } /****************************************************************************** * check_vol_busy * - busy if locked * - check_rebuild to check once more (return code indicates if it did anything) *****************************************************************************/ static Boolean check_vol_busy(struct watchedVol *watched) { Boolean rval = (watched->lock != NULL); if (!rval) rval = check_rebuild(watched, false); return rval; } /****************************************************************************** * fsys_changed gets the mach messages from notifyd * - schedule a timer (urgency detected elsewhere calls direct, canceling timer) *****************************************************************************/ static void fsys_changed(CFMachPortRef p, void *m, CFIndex size, void *info) { int result = -1; #ifdef MAC_OS_X_VERSION_10_5 uint64_t nstate; #else int nstate; #endif struct watchedVol *watched; int token; mach_msg_empty_rcv_t *msg = (mach_msg_empty_rcv_t*)m; // msg_id==token -> notify_get_state() -> watchedVol* // XX if (token == 0, perhaps a force-rebuild message?) token = msg->header.msgh_id; if (notify_get_state(token, &nstate)) goto finish; watched = (struct watchedVol*)(intptr_t)nstate; if (!watched) goto finish; // is the volume valid? (notification should have been canceled) if (CFDictionaryGetCountOfValue(sFsysWatchDict, watched)) { CFRunLoopTimerContext tc = { 0, watched, NULL, NULL, NULL }; CFAbsoluteTime firetime = CFAbsoluteTimeGetCurrent() + kWatchSettleTime; // cancel any existing timer (evidently updates are in progress) if (watched->delayer) CFRunLoopTimerInvalidate(watched->delayer); // schedule a timer to call check_now after a delay watched->delayer=CFRunLoopTimerCreate(nil,firetime,0,0,0,check_now,&tc); if (!watched->delayer) goto finish; CFRunLoopAddTimer(CFRunLoopGetCurrent(), watched->delayer, kCFRunLoopDefaultMode); CFRelease(watched->delayer); // so later invalidation will free } else kextd_error_log("invalid token/volume: %d, %p", token, watched); result = 0; finish: if (result) kextd_error_log("couldn't respond to filesystem change notification!"); return; } /****************************************************************************** * check_now, called after the timer expires, calls check_rebuild() *****************************************************************************/ void check_now(CFRunLoopTimerRef timer, void *info) { struct watchedVol *watched = (struct watchedVol*)info; // is the volume still valid? (timer should have been invalidated) if (watched && CFDictionaryGetCountOfValue(sFsysWatchDict, watched)) { watched->delayer = NULL; // timer is no longer pending (void)check_rebuild(watched, false); // don't care what it did } /* else kextd_log("DEBUG: timer for %p wasn't invalidated?", watched); */ } /****************************************************************************** * check_rebuild (indirectly) stats everything and fires kextcache as needed * returns a Boolean indicating whether anything *was* accomplished * a helper returning an error doesn't count (?) * - Boolean 'force' allows us to eventually fully handle SIGHUP * - fast path: ck_vol_busy->ck_rebuild-> newMKext/timer ->unlock->check/cancel * - if any caches are out of date, only rebuild them * - otherwise (or always if there was a stat error), copypaths->Apple_Boot's * - note: important to only rebuild out-of-date caches to prevent looping *****************************************************************************/ // kextcache -u helper sets up argv static int rebuild_boot(struct bootCaches *caches, Boolean force) { int rval = ELAST + 1; pid_t pid; int argc, argi = 0; char **kcargs; // argv[0] '-u' root NULL argc = 1 + 1 + 1 + 1; kcargs = malloc(argc * sizeof(char*)); if (!kcargs) goto finish; kcargs[argi++] = "kextcache"; if (force) { kcargs[argi++] = "-f"; } kcargs[argi++] = "-u"; kcargs[argi++] = caches->root; // kextcache reads bc.plist so nothing more needed kcargs[argi] = NULL; // terminate the list rval = 0; pid = fork_kextcache(caches->root, kcargs, false); finish: if (rval) { kextd_error_log("data error before rebuilding boot partition"); } else if (pid < 0) { rval = pid; } return rval; } /* * ?? watched->errcount (tracking cache rebuild problems on a particular volume) * could be used to prevent the case where caches are out * of date and can't be rebuilt, but we still want to copy stuff to the * boot partitions?? Otherwise, we always fire the kextcache -u, but it * might beat [regular] kextcache to the lock. * * That all said, if the mkext is straight up *gone*, we can't do much * in the way of rebuilding the data in the boot partition. * * ... after some testing, it became obvious that repeated attempts at * a failling volume would just block reboot. So we try not to let such a * volume delay reboot for long. */ static Boolean check_rebuild(struct watchedVol *watched, Boolean force) { Boolean launched = false; Boolean rebuildmkext = force; // force the mkext, nuke the stamps // if we came in some other way and there's a timer pending, cancel it if (watched->delayer) { CFRunLoopTimerInvalidate(watched->delayer); // runloop holds last ref watched->delayer = NULL; } #if 0 // no one calls us with force right now // if forcing, we need to nuke the bootstamps so that kextcache -u // updates everything after the mkext is rebuilt if (force) { char bspath[PATH_MAX]; if (snprintf(bspath, PATH_MAX, "%s/%s", watched->caches->root, kTSCacheDir) < PATH_MAX) { if (sdeepunlink(watched->caches->cachefd, bspath)) { kextd_error_log("couldn't nuke bootstamps: %d", errno); } } else { kextd_error_log("couldn't build Extensions path"); } } #endif // might have been forced above if (!rebuildmkext) rebuildmkext = check_mkext(watched->caches); // if we rebuild the mkext this time, we'll be here again on success // because we're watching for changes to the mkext file if (rebuildmkext) { if (rebuild_mkext(watched->caches, false /*wait*/)) { // logs watched->errcount++; // so we don't block reboot forever } else { launched = true; } } else { // check to see if the volume has helper partitions (and might need -u) char bsdname[DEVMAXPATHSIZE]; struct stat sb; CFDictionaryRef binfo = NULL; Boolean hasBoots, isGPT; // if not BootRoot, we don't bother with kextcache -u if (fstat(watched->caches->cachefd, &sb) == 0 && devname_r(sb.st_dev, S_IFBLK, bsdname, DEVMAXPATHSIZE) && BLCreateBooterInformationDictionary(NULL,bsdname,&binfo) == 0) { CFArrayRef ar; ar = CFDictionaryGetValue(binfo, kBLAuxiliaryPartitionsKey); hasBoots = (ar && CFArrayGetCount(ar) > 0); ar = CFDictionaryGetValue(binfo, kBLSystemPartitionsKey); isGPT = (ar && CFArrayGetCount(ar) > 0); if (hasBoots && isGPT) { Boolean anyOutOfDate = true; if (needUpdates(watched->caches,&anyOutOfDate,NULL,NULL,NULL)) { anyOutOfDate = true; } // and if necessary, run kextcache -u if (force || anyOutOfDate) { launched = (rebuild_boot(watched->caches, force) == 0); } } } if (binfo) CFRelease(binfo); } return launched; } /* ??? will we track the current root volume to treat it specially? if (rebuildplcache) KXKextManagerResetAllRepositories(gKextManager); // resets "/" // Extensions.kextcache? if (watched == rootvol) { rebuildplcache = true; // in case stat() fails if (stat(watched->plcache->rpath, &sb) == -1) goto cachecheckfailed; rebuildplcache = (sb.st_mtime == extsb.st_mtime + 1); } */ // ---- locking services (prototyped via MiG and kextmanager[_mig].defs) ---- /****************************************************************************** * kextmanager_lock_reboot locks everything (called by shutdown(8) & reboot(8)) *****************************************************************************/ static int lock_vol(struct watchedVol *watched, mach_port_t client) { int rval = ENOMEM; CFRunLoopSourceRef invalidator; CFMachPortContext mp_ctx = { 0, watched, NULL, NULL, NULL }; CFRunLoopRef rl = CFRunLoopGetCurrent(); if (!rl) goto finish; // create a new lock with the client port watched->lock = CFMachPortCreateWithPort(nil,client,NULL,&mp_ctx,false); if (!watched->lock) goto finish; CFMachPortSetInvalidationCallBack(watched->lock, lock_died); invalidator = CFMachPortCreateRunLoopSource(nil, watched->lock, 0); if (!invalidator) goto finish; CFRunLoopAddSource(rl, invalidator, kCFRunLoopDefaultMode); CFRelease(invalidator); // owned by the runloop now rval = 0; finish: return rval; } #define GIVEUPTHRESH 5 // iterator helper locking for locked or should-be-locked volumes static void check_locked(const void *key, const void *val, void *ctx) { struct watchedVol *watched = (struct watchedVol*)val; const void **bsdName = ctx; // report this one if: // it's already locked or if it needs a rebuild // but don't block reboot if it's been having errors if (watched->lock || (watched->errcount < GIVEUPTHRESH && check_rebuild(watched, false))) *bsdName = key; } kern_return_t _kextmanager_lock_reboot(mach_port_t p, mach_port_t client, dev_path_t busyDev, int *busyStatus) { kern_return_t rval = KERN_FAILURE; int result = ELAST + 1; CFStringRef bsdName = NULL; if (!busyDev || !busyStatus) { rval = KERN_SUCCESS; result = EINVAL; goto finish; } if (gClientUID != 0) { kextd_error_log("non-root doesn't need to lock or unlock volumes"); rval = KERN_SUCCESS; result = EPERM; goto finish; } if (sRebootLock) { rval = KERN_SUCCESS; // for MiG result = EBUSY; // for the client busyDev[0] = '\0'; goto finish; } // check to see if any new volumes have become eligible if (reconsiderVolumes(busyDev)) { rval = KERN_SUCCESS; // for MiG result = EBUSY; // for the client goto finish; } // if we've contacted diskarb, scan the dictionary for locked items if (sFsysWatchDict) { CFDictionaryApplyFunction(sFsysWatchDict, check_locked, &bsdName); } if (bsdName == NULL) { // great, this guy gets to lock everything (which we expect to work) CFRunLoopSourceRef invalidator; CFMachPortContext mp_ctx = { 0, &sRebootLock, 0, }; CFRunLoopRef rl = CFRunLoopGetCurrent(); if (!rl) goto finish; // create a new lock with the client port sRebootLock = CFMachPortCreateWithPort(nil,client,NULL,&mp_ctx,false); if (!sRebootLock) goto finish; CFMachPortSetInvalidationCallBack(sRebootLock, lock_died); invalidator = CFMachPortCreateRunLoopSource(nil, sRebootLock, 0); if (!invalidator) goto finish; CFRunLoopAddSource(rl, invalidator, kCFRunLoopDefaultMode); CFRelease(invalidator); // owned by the runloop now result = 0; // not locked } else { // bsdName (at least) was locked, try again later result = EBUSY; if(!CFStringGetFileSystemRepresentation(bsdName, busyDev, DEVMAXPATHSIZE)) busyDev[0] = '\0'; } rval = KERN_SUCCESS; finish: if (rval == KERN_SUCCESS) { *busyStatus = result; } else { kextd_error_log("error locking for reboot"); } if (result == EBUSY && bsdName) kextd_log("%s was busy, preventing lock for reboot", busyDev); return rval; } /****************************************************************************** * _kextmanager_lock_volume tries to lock volumes for clients (i.e. kextcache) * - volDev is the "bsdName" of the volume in question *****************************************************************************/ kern_return_t _kextmanager_lock_volume(mach_port_t p, mach_port_t client, dev_path_t volDev, int *lockstatus) { kern_return_t rval = KERN_FAILURE; int result; CFStringRef bsdName = NULL; struct watchedVol *watched = NULL; struct statfs sfs; if (!lockstatus) { kextd_error_log("kextmanager_lock_volume requires lockstatus != NULL"); rval = KERN_SUCCESS; result = EINVAL; } if (gClientUID != 0 /*watched->fsinfo->f_owner ?*/) { kextd_error_log("non-root doesn't need to lock or unlock volumes"); rval = KERN_SUCCESS; result = EPERM; goto finish; } // if we're still trying to initialize or we're rebooting, // deny any new locks if (!sFsysWatchDict || sRebootLock) { rval = KERN_SUCCESS; // for MiG result = EBUSY; // for the client goto finish; } result = ENOMEM; bsdName = CFStringCreateWithFileSystemRepresentation(nil, volDev); if (!bsdName) goto finish; watched = (void*)CFDictionaryGetValue(sFsysWatchDict, bsdName); if (!watched) { rval = KERN_SUCCESS; result = ENOENT; goto finish; } if (watched->lock) { // kextd_log("DEBUG: volume %s locked", watched->caches->root); result = EBUSY; } else { if (lock_vol(watched, client)) goto finish; result = 0; } // try to enable owners if not currently honored if (statfs(watched->caches->root, &sfs) == 0 && (sfs.f_flags & MNT_IGNORE_OWNERSHIP)) { toggleOwners(volDev, true); watched->disableOwners = true; } rval = KERN_SUCCESS; finish: if (bsdName) CFRelease(bsdName); if (rval) { if (gClientUID == 0) kextd_error_log("trouble while locking %s", volDev); cleanupLock(watched->lock); } else { *lockstatus = result; // only meaningful on rval == 0 } return rval; } /****************************************************************************** * _kextmanager_unlock_volume unlocks for clients (i.e. kextcache) *****************************************************************************/ kern_return_t _kextmanager_unlock_volume(mach_port_t p, mach_port_t client, dev_path_t volDev, int exitstatus) { kern_return_t rval = KERN_FAILURE; CFStringRef bsdName = NULL; struct watchedVol *watched = NULL; // since we don't need the extra send right added by MiG if (mach_port_deallocate(mach_task_self(), client)) goto finish; if (gClientUID != 0 /*watched->fsinfo->f_owner ?*/) { kextd_error_log("non-root doesn't need to lock or unlock volumes"); rval = KERN_SUCCESS; goto finish; } // make sure we're set up if (!sFsysWatchDict) goto finish; bsdName = CFStringCreateWithFileSystemRepresentation(nil, volDev); if (!bsdName) goto finish; watched = (void*)CFDictionaryGetValue(sFsysWatchDict, bsdName); if (!watched) goto finish; if (!watched->lock) { kextd_error_log("%s isn't locked", watched->caches->root); goto finish; } if (client != CFMachPortGetPort(watched->lock)) { kextd_error_log("%p not used to lock %s", client, watched->caches->root); goto finish; } // okay, recording any error and releasing the lock if (exitstatus) { if (exitstatus == EX_TEMPFAIL) { // kextcache not done yet; so don't record error } else { kextd_log("kextcache reported a problem updating %s", volDev); watched->errcount++; } } else if (watched->errcount) { // put reassuring message in the log kextd_log("kextcache succeeded with %s (previously failed)", volDev); watched->errcount = 0; } // disable owners if we enabled them for the locker if (watched->disableOwners) { toggleOwners(volDev, false); watched->disableOwners = false; } cleanupLock(watched->lock); /* could try to speed things along (-m to -u; 5 seconds faster at shutdown) * disabled with mutli-lock/unlock for owners; check EX_TEMPFAIL in Leopard if (watched->errcount < GIVEUPTHRESH) (void)check_rebuild(watched, false); */ rval = KERN_SUCCESS; finish: if (bsdName) CFRelease(bsdName); if (rval && watched) { kextd_error_log("couldn't unlock %s", watched->caches->root); } return rval; } /****************************************************************************** * lock_died tells us when the receive right went away * - this is okay if we're currently unlocked; bad otherwise *****************************************************************************/ static void lock_died(CFMachPortRef p, void *info) { struct watchedVol* watched = (struct watchedVol*)info; if (info == &sRebootLock) { kextd_log("reboot/shutdown should have rebooted instead of dying"); cleanupLock(sRebootLock); // there is explicit releasing this lock } else if (!watched) { kextd_error_log("lock_died: NULL info??"); } else if (CFDictionaryGetCountOfValue(sFsysWatchDict, watched) == 0) { // volume might have been renamed while in action (4620558) // kextd_log("no container for invalid lock"); // expected via vol_gone? } else if (watched->lock) { kextd_error_log("child exited w/o releasing lock on %s", watched->caches->root); // try to disable owners if we enabled them for the locker if (watched->disableOwners) { struct statfs sfs; char *bsdname; if (statfs(watched->caches->root, &sfs) == 0 && (bsdname = strstr(sfs.f_mntfromname, "disk"))) { toggleOwners(bsdname, false); watched->disableOwners = false; } } cleanupLock(watched->lock); // could clean up worker pid if we were actually storing it } /*else { // kextd_log("DEBUG: lock (cleanly) deallocated for %s", watched->caches->root); }*/ } /****************************************************************************** * reconsiderVolume() rechecks to see if a volume has become interesting * given that we watch owners-ignored volumes, only needed for OS copies *****************************************************************************/ static Boolean reconsiderVolume(dev_path_t volDev) { int result = -1; Boolean rval = false; CFStringRef bsdName = NULL; struct watchedVol *watched; DADiskRef disk = NULL; bsdName = CFStringCreateWithCString(nil, volDev, kCFStringEncodingASCII); if (!bsdName) goto finish; // lock_reboot will shortly call check_rebuild on all watched volumes if (!CFDictionaryGetValue(sFsysWatchDict, bsdName)) { if (!(disk = DADiskCreateFromBSDName(nil, sDASession, volDev))) goto finish; // maybe we should be watching it now? vol_appeared(disk, NULL); if ((watched = (void*)CFDictionaryGetValue(sFsysWatchDict, bsdName))) { rval = check_rebuild(watched, false); // vol_appeared calling too } } result = 0; finish: if (disk) CFRelease(disk); if (bsdName) CFRelease(bsdName); if (result) { kextd_error_log("error reconsidering volume %d"); } return rval; } /****************************************************************************** * reconsiderVolumes() iterates the mount list, reconsidering all local mounts * if any one of them needed an update, busyDev is set to the disk in question *****************************************************************************/ static Boolean reconsiderVolumes(dev_path_t busyDev) { Boolean rval = false; char *errmsg = NULL; int nfsys, i; size_t bufsz; struct statfs *mounts; char *bsdname; // if not set up ... if (!sDASession) goto finish; errmsg = "error while getting mount list"; if (-1 == (nfsys = getfsstat(NULL, 0, 0))) goto finish; bufsz = nfsys * sizeof(struct statfs); if (!(mounts = malloc(bufsz))) goto finish; if (-1 == getfsstat(mounts, bufsz, MNT_NOWAIT)) goto finish; errmsg = NULL; // let reconsiderVolume() take it from here for (i = 0; i < nfsys; i++) { struct statfs *sfs = &mounts[i]; if (sfs->f_flags & MNT_LOCAL && (bsdname = strstr(sfs->f_mntfromname, "disk"))) { if (reconsiderVolume(bsdname)) { rval = true; strlcpy(busyDev, bsdname, DEVMAXPATHSIZE); } } } errmsg = NULL; finish: if (errmsg) kextd_error_log(errmsg); return rval; } /****************************************************************************** * toggleOwners() enables or disables owners as requested *****************************************************************************/ static void toggleOwners(dev_path_t volDev, Boolean enableOwners) { int result = ELAST + 1; DASessionRef session = NULL; CFStringRef toggleMode = CFSTR("toggleOwnersMode"); DADiskRef disk = NULL; DADissenterRef dis = (void*)kCFNull; CFStringRef mountargs[] = { CFSTR("update"), NULL, NULL }; if (enableOwners) { mountargs[1] = CFSTR("perm"); } else { mountargs[1] = CFSTR("noperm"); } // same 'dis' logic as mountBoot in update_boot.c if (!(session = DASessionCreate(nil))) goto finish; DASessionScheduleWithRunLoop(session, CFRunLoopGetCurrent(), toggleMode); if (!(disk = DADiskCreateFromBSDName(nil, session, volDev))) goto finish; DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, _daDone, &dis, mountargs); while (dis == (void*)kCFNull) { CFRunLoopRunInMode(toggleMode, 0, true); // _daDone updates 'dis' } if (dis) goto finish; result = 0; finish: if (dis && dis != (void*)kCFNull) CFRelease(dis); if (disk) CFRelease(disk); if (session) CFRelease(session); if (result) kextd_log("WARNING: couldn't %s owners for %s", enableOwners ? "enable":"disable", volDev); } /******************************************************************************* * updateRAIDSet() -- Something on a RAID set has changed, so we may need to * update its boot partition info. *******************************************************************************/ #define RAID_MATCH_SIZE (2) void updateRAIDSet( CFNotificationCenterRef center, void * observer, CFStringRef name, const void * object, CFDictionaryRef userInfo) { char * errorMessage = NULL; CFStringRef matchingKeys[RAID_MATCH_SIZE] = { CFSTR("RAID"), CFSTR("UUID") }; CFTypeRef matchingValues[RAID_MATCH_SIZE] = { (CFTypeRef)kCFBooleanTrue, (CFTypeRef)object }; CFDictionaryRef matchPropertyDict = NULL; CFMutableDictionaryRef matchingDict = NULL; io_service_t theRAIDSet = MACH_PORT_NULL; CFStringRef bsdName = NULL; struct watchedVol * watched = NULL; // do not free // nothing to do if we're not watching yet if (!sFsysWatchDict) goto finish; errorMessage = "No RAID set named in RAID set changed notification."; if (!object) { goto finish; } errorMessage = "Unable to create matching dictionary for RAID set."; matchPropertyDict = CFDictionaryCreate(kCFAllocatorDefault, (const void **)&matchingKeys, (const void **)&matchingValues, RAID_MATCH_SIZE, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (!matchPropertyDict) { goto finish; } matchingDict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (!matchingDict) { goto finish; } CFDictionarySetValue(matchingDict, CFSTR(kIOPropertyMatchKey), matchPropertyDict); errorMessage = NULL; // maybe the RAID just went away theRAIDSet = IOServiceGetMatchingService(kIOMasterPortDefault, matchingDict); matchingDict = NULL; // IOServiceGetMatchingService() consumes reference! if (!theRAIDSet) { goto finish; } errorMessage = "Missing BSD Name for updated RAID set."; bsdName = IORegistryEntryCreateCFProperty(theRAIDSet, CFSTR("BSD Name"), kCFAllocatorDefault, 0); if (!bsdName) { goto finish; } watched = (void*)CFDictionaryGetValue(sFsysWatchDict, bsdName); if (watched) { (void)rebuild_boot(watched->caches, true /* force rebuild */); } errorMessage = NULL; finish: if (errorMessage) { kextd_error_log(errorMessage); } if (matchPropertyDict) CFRelease(matchPropertyDict); if (matchingDict) CFRelease(matchingDict); if (theRAIDSet) IOObjectRelease(theRAIDSet); if (bsdName) CFRelease(bsdName); return; }