/* * Copyright (c) 2007 by Apple Computer, Inc., All Rights Reserved. * Copyright (C) 2011-2013 Sourcefire, Inc. */ #include <kern/assert.h> #include <mach/mach_types.h> #include <libkern/libkern.h> #include <libkern/OSAtomic.h> #include <libkern/OSMalloc.h> #include <sys/sysctl.h> #include <sys/systm.h> #include <sys/kauth.h> #include <sys/vnode.h> #include <sys/uio.h> #include <sys/conf.h> #include <miscfs/devfs/devfs.h> #define CLAMAUTH_VERSION "0.3" #define CLAMAUTH_PROTOCOL_VERSION 2 #pragma mark ***** Global Resources /* These declarations are required to allocate memory and create locks. * They're created when we start and destroyed when we stop. */ static OSMallocTag gMallocTag = NULL; static lck_grp_t * gLockGroup = NULL; #define CLAMAUTH_EVENTS (KAUTH_VNODE_EXECUTE) struct AuthEvent { /* don't change the first two fields */ UInt32 action; char path[1024]; UInt32 pid; }; #define EVENTQSIZE 64 struct AuthEventQueue { struct AuthEvent queue[EVENTQSIZE]; int cnt, first, last; }; void AuthEventInitQueue(struct AuthEventQueue *queue); void AuthEventEnqueue(struct AuthEventQueue *queue, struct AuthEvent *event); int AuthEventDequeue(struct AuthEventQueue *queue, struct AuthEvent *event); void AuthEventInitQueue(struct AuthEventQueue *queue) { memset(queue, 0, sizeof(struct AuthEventQueue)); queue->first = queue->cnt = 0; queue->last = EVENTQSIZE - 1; } void AuthEventEnqueue(struct AuthEventQueue *queue, struct AuthEvent *event) { queue->last = (queue->last + 1) % EVENTQSIZE; memcpy(&queue->queue[queue->last], event, sizeof(struct AuthEvent)); queue->cnt++; } int AuthEventDequeue(struct AuthEventQueue *queue, struct AuthEvent *event) { if(!queue->cnt) return 1; memcpy(event, &queue->queue[queue->first], sizeof(struct AuthEvent)); queue->first = (queue->first + 1) % EVENTQSIZE; queue->cnt--; return 0; } struct AuthEventQueue gEventQueue; static lck_mtx_t *gEventQueueLock = NULL; static SInt32 gEventCount = 0; #define MAX_PREFIX_NUM 10 #define MAX_PREFIX_LEN 128 static char gPrefixTable[MAX_PREFIX_NUM][MAX_PREFIX_LEN]; static unsigned int gPrefixCount = 0; static int CreateVnodePath(vnode_t vp, char **vpPathPtr) /* Creates a full path for a vnode. vp may be NULL, in which * case the returned path is NULL (that is, no memory is allocated). * vpPathPtr is a place to store the allocated path buffer. * The caller is responsible for freeing this memory using OSFree * (the size is always MAXPATHLEN). */ { int err; int pathLen; assert( vpPathPtr != NULL); assert(*vpPathPtr == NULL); err = 0; if (vp != NULL) { *vpPathPtr = OSMalloc(MAXPATHLEN, gMallocTag); if (*vpPathPtr == NULL) { err = ENOMEM; } if (err == 0) { pathLen = MAXPATHLEN; err = vn_getpath(vp, *vpPathPtr, &pathLen); } } return err; } /* /dev/clamauth handling */ static int ca_devidx = -1; static void *ca_devnode = NULL; int dev_open = 0, dev_read = 0; static int ca_open(dev_t dev, int flag, int devtype, proc_t p) { if(dev_open) return EBUSY; dev_open = 1; return 0; } static int ca_close(dev_t dev, int flag, int devtype, proc_t p) { struct AuthEvent event; lck_mtx_lock(gEventQueueLock); dev_open = 0; dev_read = 0; AuthEventInitQueue(&gEventQueue); /* Initialize event queue and add version info event */ event.action = CLAMAUTH_PROTOCOL_VERSION; strncpy(event.path, "ClamAuth "CLAMAUTH_VERSION"", sizeof(event.path)); event.pid = 0xdeadbeef; AuthEventEnqueue(&gEventQueue, &event); lck_mtx_unlock(gEventQueueLock); return 0; } static int ca_read(dev_t dev, uio_t uio, int ioflag) { int ret = 0, size, retq = 0; struct AuthEvent event; struct timespec waittime; waittime.tv_sec = 1; waittime.tv_nsec = 0; while(uio_resid(uio) > 0) { lck_mtx_lock(gEventQueueLock); retq = AuthEventDequeue(&gEventQueue, &event); dev_read = 1; lck_mtx_unlock(gEventQueueLock); if(retq != 1) { /* snprintf(info, sizeof(info), "PATH: %s, PID: %d, ACTION: %d\n", event.path, event.pid, event.action); */ size = MIN(uio_resid(uio), sizeof(event)); ret = uiomove((const char *) &event, size, uio); if(ret) break; } else { //(void) msleep(&gEventQueue, NULL, PUSER, "events", &waittime); break; } } if(ret) { printf("ClamAuth: uiomove() failed\n"); } return ret; } static int ca_write(dev_t dev, uio_t uio, int ioflag) { return EBADF; } static int ca_ioctl(dev_t dev, u_long cmd, caddr_t addr, int flag, proc_t p) { return EBADF; } static int ca_select(dev_t dev, int flag, void * wql, proc_t p) { return EBADF; } static struct cdevsw clamauth_cdevsw = { ca_open, ca_close, ca_read, ca_write, ca_ioctl, eno_stop, eno_reset, NULL, ca_select, eno_mmap, eno_strat, eno_getc, eno_putc, 0 }; static int ca_remove(void) { if(ca_devnode) devfs_remove(ca_devnode); if(ca_devidx != -1) { if(cdevsw_remove(ca_devidx, &clamauth_cdevsw) != ca_devidx) { printf("ClamAuth: cdevsw_remove() failed\n"); return KERN_FAILURE; } } return KERN_SUCCESS; } #pragma mark ***** Listener Resources /* Some scopes (for example KAUTH_SCOPE_VNODE) are called a /lot/. Thus, * it's a good idea to avoid taking mutexes in your listener if at all * possible. Thus, we use non-blocking synchronisation to protect the * global data that's accessed by our listener (gPrefix). * Every time we enter a listener, we increment gActivationCount, and ever * time we leave we decrement it. When we want to change the listener, we * first remove the listener, then we wait for the activation count to hit, * then we can modify the globals protected by that activation count. * * IMPORTANT: * There is still a race condition here. See RemoveListener for a description * of the race and why we can't fix it. */ static SInt32 gActivationCount = 0; static int VnodeScopeListener( kauth_cred_t credential, void * idata, kauth_action_t action, uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3 ) /* A Kauth listener that's called to authorize an action in the vnode scope */ { #pragma unused(credential) #pragma unused(idata) #pragma unused(arg3) int err; vfs_context_t context; vnode_t vp; vnode_t dvp; char * vpPath; char * dvpPath; struct AuthEvent event; unsigned int i, mpath = 0; (void) OSIncrementAtomic(&gActivationCount); context = (vfs_context_t) arg0; vp = (vnode_t) arg1; dvp = (vnode_t) arg2; vpPath = NULL; dvpPath = NULL; /* Convert the vnode, if any, to a path. */ err = CreateVnodePath(vp, &vpPath); /* Convert the parent directory vnode, if any, to a path. */ if (err == 0) err = CreateVnodePath(dvp, &dvpPath); /* Tell the user about this request. Note that we filter requests * based on gPrefix. If gPrefix is set, only requests where one * of the paths is prefixed by gPrefix will be printed. */ if (err == 0) { for(i = 0; i < gPrefixCount; i++) { if(vpPath && strprefix(vpPath, gPrefixTable[i])) { mpath = 1; } else if(dvpPath && strprefix(dvpPath, gPrefixTable[i])) { mpath = 1; } if(mpath) break; } if (mpath) { if(action & CLAMAUTH_EVENTS) printf( "scope=" KAUTH_SCOPE_VNODE ", uid=%ld, vp=%s, dvp=%s\n", (long) kauth_cred_getuid(vfs_context_ucred(context)), (vpPath != NULL) ? vpPath : "<null>", (dvpPath != NULL) ? dvpPath : "<null>" ); event.pid = vfs_context_pid(context); event.action = action; if(vpPath) { strncpy(event.path, vpPath, sizeof(event.path)); event.path[sizeof(event.path) - 1] = 0; } else { event.path[0] = 0; } lck_mtx_lock(gEventQueueLock); if(dev_read && (action & CLAMAUTH_EVENTS)) { // printf("gPrefix: %s, vpPath: %s, dvpPath: %s, action: %d\n", gPrefix, vpPath ? vpPath : "<null>", dvpPath ? dvpPath : "<null>", action); AuthEventEnqueue(&gEventQueue, &event); } lck_mtx_unlock(gEventQueueLock); (void) OSIncrementAtomic(&gEventCount); } } else { printf("ClamAuth.VnodeScopeListener: Error %d.\n", err); } if (vpPath != NULL) { OSFree(vpPath, MAXPATHLEN, gMallocTag); } if (dvpPath != NULL) { OSFree(dvpPath, MAXPATHLEN, gMallocTag); } (void) OSDecrementAtomic(&gActivationCount); return KAUTH_RESULT_DEFER; } static int FileOpScopeListener( kauth_cred_t credential, void * idata, kauth_action_t action, uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3 ) /* A Kauth listener that's called to authorize an action in the file operation */ { #pragma unused(credential) #pragma unused(idata) #pragma unused(arg2) #pragma unused(arg3) struct AuthEvent event; vfs_context_t context; const char *path; unsigned int i, mpath = 0; if(!dev_read) return KAUTH_RESULT_DEFER; context = (vfs_context_t) arg0; path = (const char *) arg1; (void) OSIncrementAtomic(&gActivationCount); switch (action) { /* case KAUTH_FILEOP_OPEN: */ case KAUTH_FILEOP_EXEC: for(i = 0; i < gPrefixCount; i++) { if(strprefix((const char *) arg1, gPrefixTable[i])) { mpath = 1; break; } } if(mpath) { event.pid = vfs_context_pid(context); event.action = action; strncpy(event.path, path, sizeof(event.path)); event.path[sizeof(event.path) - 1] = 0; lck_mtx_lock(gEventQueueLock); if(dev_read) AuthEventEnqueue(&gEventQueue, &event); lck_mtx_unlock(gEventQueueLock); } break; default: break; } (void) OSDecrementAtomic(&gActivationCount); return KAUTH_RESULT_DEFER; } #pragma mark ***** Listener Install/Remove /* gConfigurationLock is a mutex that protects us from two threads trying to * simultaneously modify the configuration. The configuration is protect in * N ways: * * o During startup, we register our sysctl OID last, so no one can start * modifying the configuration until everything is set up nicely. * * o During normal operations, the sysctl handler (SysctlHandler) takes * the lock to prevent two threads from reconfiguring the system at the * same time. * * o During termination, the stop routine first removes the sysctl OID * and then takes the lock before it removes the listener. The first * act prevents any new sysctl requests coming it, the second blocks * until current sysctl requests are done. * * IMPORTANT: * There is still a race condition here. See the stop routine for a description * of the race and why we can't fix it. */ static lck_mtx_t * gConfigurationLock = NULL; /* gListener is our handle to the installed scope listener. We need to * keep it around so that we can remove the listener when we're done. */ static kauth_listener_t gListener = NULL; static void RemoveListener(void) /* Removes the installed scope listener, if any. * * Under almost all circumstances this routine runs under the * gConfigurationLock. The only time that this might not be the case * is when the KEXT's start routine fails prior to gConfigurationLock * being created. */ { /* First prevent any more threads entering our listener. */ if (gListener != NULL) { kauth_unlisten_scope(gListener); gListener = NULL; } /* Then wait for any threads within out listener to stop. Note that there * is still a race condition here; there could still be a thread executing * between the OSDecrementAtomic and the return from the listener function * (for example, FileOpScopeListener). However, there's no way to close * this race because of the weak concurrency guarantee for kauth_unlisten_scope. * Moreover, the window is very small and, seeing as this only happens during * reconfiguration, I'm not too worried. However, I am worried enough * to ensure that this loop runs at least once, so we always delay the teardown * for at least one second waiting for the threads to drain from our * listener. */ do { struct timespec oneSecond; oneSecond.tv_sec = 1; oneSecond.tv_nsec = 0; (void) msleep(&gActivationCount, NULL, PUSER, "com_apple_dts_kext_ClamAuth.RemoveListener", &oneSecond); } while ( gActivationCount > 0 ); } static void InstallListener(void) /* Installs a listener for the specified scope. scope and scopeLen specifies * the scope to listen for. prefix is a parameter for the scope listener. * It may be NULL. * * prefix points into the gConfiguration global variable, so this routine * doesn't make a copy of it. However, it has to make a copy of scope * because scope can point to a place in the middle of the gConfiguration * variable, so there's no guarantee it's null terminated (which we need it * to be in order to call kauth_listen_scope. * * This routine always runs under the gConfigurationLock. */ { assert(gListener == NULL); //gListener = kauth_listen_scope(KAUTH_SCOPE_VNODE, VnodeScopeListener, NULL); gListener = kauth_listen_scope(KAUTH_SCOPE_FILEOP, FileOpScopeListener, NULL); if (gListener == NULL) { printf("ClamAuth.InstallListener: Could not create gListener.\n"); RemoveListener(); } else { printf("ClamAuth: Installed file listener\n"); } } static void ConfigureKauth(const char *configuration) /* This routine is called by the sysctl handler when it notices * that the configuration has changed. It's responsible for * parsing the new configuration string and updating the listener. * * See SysctlHandler for a description of how I chose to handle the * failure case. * * This routine always runs under the gConfigurationLock. */ { unsigned int i = 0; assert(configuration != NULL); /* Remove the existing listener. */ RemoveListener(); /* Parse the configuration string and install the new listener. */ if (strcmp(configuration, "remove") == 0) { printf("ClamAuth.ConfigureKauth: Removed listener.\n"); } else if ( strprefix(configuration, "monitor ") ) { const char *cursor; /* Skip the "monitor ". */ cursor = configuration + strlen("monitor "); gPrefixCount = 0; while(*cursor == ' ') cursor++; if (!*cursor) { printf("ClamAuth.ConfigureKauth: Bad configuration '%s'.\n", configuration); return; } while(1) { if(i < MAX_PREFIX_LEN - 1) { if(*cursor == ' ') { gPrefixTable[gPrefixCount][i] = 0; gPrefixCount++; i = 0; if(gPrefixCount >= MAX_PREFIX_NUM) { printf("ClamAuth.ConfigureKauth: Too many paths (> %u).\n", MAX_PREFIX_NUM); gPrefixCount = 0; return; } } else { gPrefixTable[gPrefixCount][i++] = *cursor; } } else { printf("ClamAuth.ConfigureKauth: Path too long (%u > %u).\n", i, MAX_PREFIX_LEN); gPrefixCount = 0; return; } cursor++; if(!*cursor) { gPrefixTable[gPrefixCount][i] = 0; gPrefixCount++; break; } } printf("ClamAuth.ConfigureKauth: Monitoring %u path(s)\n", gPrefixCount); InstallListener(); } } /* gConfiguration holds our current configuration string. It's modified by * SysctlHandler (well, by sysctl_handle_string which is called by SysctlHandler). */ static char gConfiguration[1024]; static int SysctlHandler( struct sysctl_oid * oidp, void * arg1, int arg2, struct sysctl_req * req ) /* This routine is called by the kernel when the user reads or * writes our sysctl variable. The arguments are standard for * a sysctl handler. */ { int result; /* Prevent two threads trying to change our configuration at the same * time. */ lck_mtx_lock(gConfigurationLock); /* Let sysctl_handle_string do all the heavy lifting of getting * and setting the variable. */ result = sysctl_handle_string(oidp, arg1, arg2, req); /* On the way out, if we got no error and a new value was set, * do our magic. */ if ( (result == 0) && (req->newptr != 0) ) { ConfigureKauth(gConfiguration); } lck_mtx_unlock(gConfigurationLock); return result; } /* Declare our sysctl OID (that is, a variable that the user can * get and set using sysctl). Once this OID is registered (which * is done in the start routine, ClamAuth_start, below), the user * user can get and set our configuration variable (gConfiguration) * using the sysctl command line tool. * * We use OID using SYSCTL_OID rather than SYSCTL_STRING because * we want to override the hander function that's call (we want * SysctlHandler rather than sysctl_handle_string). */ SYSCTL_OID( _kern, /* parent OID */ OID_AUTO, /* sysctl number, OID_AUTO means we're only accessible by name */ com_apple_dts_kext_ClamAuth, /* our name */ CTLTYPE_STRING | CTLFLAG_RW | CTLFLAG_KERN, /* we're a string, more or less */ gConfiguration, /* sysctl_handle_string gets/sets this string */ sizeof(gConfiguration), /* and this is its maximum length */ SysctlHandler, /* our handler */ "A", /* because that's what SYSCTL_STRING does */ "" /* just a comment */ ); /* gRegisteredOID tracks whether we've registered our OID or not. */ static boolean_t gRegisteredOID = FALSE; #pragma mark ***** Start/Stop /* Prototypes for our entry points */ extern kern_return_t com_apple_dts_kext_ClamAuth_start(kmod_info_t * ki, void * d); extern kern_return_t com_apple_dts_kext_ClamAuth_stop(kmod_info_t * ki, void * d); extern kern_return_t com_apple_dts_kext_ClamAuth_start(kmod_info_t * ki, void * d) /* Called by the system to start up the kext. */ { #pragma unused(ki) #pragma unused(d) kern_return_t err; struct AuthEvent event; ca_devidx = cdevsw_add(-1, &clamauth_cdevsw); if(ca_devidx == -1) { printf("ClamAuth: cdevsw_add() failed\n"); return KERN_FAILURE; } ca_devnode = devfs_make_node(makedev(ca_devidx, 0), DEVFS_CHAR, UID_ROOT, GID_WHEEL, 0660, "clamauth"); if(!ca_devnode) { printf("ClamAuth: Can't create /dev/clamauth\n"); return ca_remove(); } /* Allocate our global resources, needed in order to allocate memory * and locks throughout the rest of the program. */ err = KERN_SUCCESS; gMallocTag = OSMalloc_Tagalloc("com.apple.dts.kext.ClamAuth", OSMT_DEFAULT); if (gMallocTag == NULL) { err = KERN_FAILURE; } if (err == KERN_SUCCESS) { gLockGroup = lck_grp_alloc_init("com.apple.dts.kext.ClamAuth", LCK_GRP_ATTR_NULL); if (gLockGroup == NULL) { err = KERN_FAILURE; } } /* Allocate the lock that protects our configuration. */ if (err == KERN_SUCCESS) { gConfigurationLock = lck_mtx_alloc_init(gLockGroup, LCK_ATTR_NULL); if (gConfigurationLock == NULL) { err = KERN_FAILURE; } } /* Event queue lock */ if (err == KERN_SUCCESS) { gEventQueueLock = lck_mtx_alloc_init(gLockGroup, LCK_ATTR_NULL); if (gEventQueueLock == NULL) { err = KERN_FAILURE; } } AuthEventInitQueue(&gEventQueue); /* Initialize event queue and add version info event */ event.action = CLAMAUTH_PROTOCOL_VERSION; strncpy(event.path, "ClamAuth "CLAMAUTH_VERSION"", sizeof(event.path)); event.pid = 0xdeadbeef; AuthEventEnqueue(&gEventQueue, &event); /* Register our sysctl handler. */ if (err == KERN_SUCCESS) { sysctl_register_oid(&sysctl__kern_com_apple_dts_kext_ClamAuth); gRegisteredOID = TRUE; } /* If we failed, shut everything down. */ if (err != KERN_SUCCESS) { printf("ClamAuth_start: Failed to initialize the driver\n"); (void) com_apple_dts_kext_ClamAuth_stop(ki, d); } else printf("ClamAuth_start: ClamAV kernel driver loaded\n"); return err; } extern kern_return_t com_apple_dts_kext_ClamAuth_stop(kmod_info_t * ki, void * d) /* Called by the system to shut down the kext. */ { #pragma unused(ki) #pragma unused(d) int ret; /* Remove our sysctl handler. This prevents more threads entering the * handler and trying to change the configuration. There is still a * race condition here though. If a thread is already running in our * sysctl handler, there's no way to guarantee that it's done before * we destroy key resources (notably the gConfigurationLock mutex) that * it depends on. That's because sysctl_unregister_oid makes no attempt * to wait until all threads running inside the OID handler are done * before it returns. I could do stuff to minimise the risk, but there's * is no 100% way to close this race so I'm going to ignore it. */ if (gRegisteredOID) { sysctl_unregister_oid(&sysctl__kern_com_apple_dts_kext_ClamAuth); gRegisteredOID = FALSE; } /* remove the character device */ ret = ca_remove(); /* Shut down the scope listen, if any. Not that we lock gConfigurationLock * because RemoveListener requires it to be locked. Further note that * we only do this if the lock has actually been allocated. If the startup * routine fails, we can get called with gConfigurationLock set to NULL. */ if (gConfigurationLock != NULL) { lck_mtx_lock(gConfigurationLock); } RemoveListener(); if (gConfigurationLock != NULL) { lck_mtx_unlock(gConfigurationLock); } /* Clean up the configuration lock. */ if (gConfigurationLock != NULL) { lck_mtx_free(gConfigurationLock, gLockGroup); gConfigurationLock = NULL; } /* Clean up the event queue lock. */ if (gEventQueueLock != NULL) { lck_mtx_free(gEventQueueLock, gLockGroup); gEventQueueLock = NULL; } /* Clean up our global resources. */ if (gLockGroup != NULL) { lck_grp_free(gLockGroup); gLockGroup = NULL; } if (gMallocTag != NULL) { OSMalloc_Tagfree(gMallocTag); gMallocTag = NULL; } printf("ClamAuth_stop: ClamAV kernel driver removed\n"); return ret; }