The license model is a BSD Open Source License. This is a non-viral license, only asking that if you use it, you acknowledge the authors, in this case Slava Imameev.
The project uses the distorm disassembler https://github.com/gdabah/distorm which is now released under BSD license.
This is an I/O file system isolation filter for MacOS ( Mac OS X ). The idea is to intercept read and write requests and redirect them to an alternative storage. This provides an isolation layer for data flow. The possible applications for a filter are content analyzing, encryption or any advanced data flow modification.
This project is a proof of concept and has never been a subject of thorough testing.
The filter is based on the following projects
https://github.com/slavaim/MacOSX-FileSystem-Filter
https://github.com/slavaim/MacOSX-SparseFile-KernelMode
The MacOSX-FileSystem-Filter project is used to implement filtering for VFS layer. The MacOSX-SparseFile-KernelMode is used to provide an alternative storage as a sparse file.
The implementation uses a technique similar to a stackable file system by initialising vnode objects to take control over file caching and mapped file operations. Such vnode is called a covering vnode as it covers(isolates) a vnode created by an underlying file system from I/O operations. The covering vnode technique allows to implement a very flexible control over files as the isolation layer has a full control over vnodes visible to applications. The similar isolation technique for Windows is described in a series of articles from OSR - http://www.osronline.com/article.cfm?article=560 and http://www.osronline.com/article.cfm?article=571 . In case of Windows a file object (FILE_OBJECT) is initialized by the isolation layer.
When the isolation filter detects a lookup operation it creates a covering vnode, below is a call stack for this case
thread #9: tid = 0x1ba6, 0xffffff7faa7577fe FileSystem-Isolation`VifCoveringFsd::VifFsdHookCreateCoveringVnodeInternal(this=0xffffff803d959dc0, ap=0xffffff80c07039f8) + 30 at VifCoveringVnode.cpp:162, name = '0xffffff80356c2a80', queue = '0x0', stop reason = breakpoint 2.1
frame #0: 0xffffff7faa7577fe FileSystem-Isolation`VifCoveringFsd::VifFsdHookCreateCoveringVnodeInternal(this=0xffffff803d959dc0, ap=0xffffff80c07039f8) + 30 at VifCoveringVnode.cpp:162
frame #1: 0xffffff7faa757cf3 FileSystem-Isolation`VifCoveringFsd::VifReplaceVnodeByCovering(this=0xffffff803d959dc0, vnodeToCover=0xffffff80c0703d60, dvp=0xffffff8042970b40) + 419 at VifCoveringVnode.cpp:437
frame #2: 0xffffff7faa76a25b FileSystem-Isolation`VifFsdLookupHook(ap=0xffffff80c0703b18) + 395 at VNodeHook.cpp:230
frame #3: 0xffffff802813f2f8 kernel`lookup(ndp=0xffffff80c0703d38) + 968 at kpi_vfs.c:2783
frame #4: 0xffffff802813ea95 kernel`namei(ndp=0xffffff80c0703d38) + 1941 at vfs_lookup.c:371
frame #5: 0xffffff8028152005 kernel`nameiat(ndp=0xffffff80c0703d38, dirfd=<unavailable>) + 133 at vfs_syscalls.c:2920
frame #6: 0xffffff8028129fd6 kernel`getattrlistat_internal(ctx=0xffffff80356a5970, path=<unavailable>, alp=0xffffff80c0703f28, attributeBuffer=4476501328, bufferSize=958, options=37, segflg=<unavailable>, pathsegflg=<unavailable>, fd=<unavailable>) + 294 at vfs_attrlist.c:2755
frame #7: 0xffffff8028123227 kernel`getattrlist(p=<unavailable>, uap=<unavailable>, retval=<unavailable>) + 119 at vfs_attrlist.c:2787
frame #8: 0xffffff802844dcb2 kernel`unix_syscall64(state=0xffffff8035484ce0) + 610 at systemcalls.c:366
For VFS operation that requires to pass through request to an underlying(original) file system the isolation layer converts covering vnode to an original vnode to call the underlying file system, for example below is a stack for a call to retrieve file attributes
frame #3: 0xffffff7fae557ea2 FileSystem-Isolation`VifCoveringFsd::GetCoveringFsd(mnt=0xffffff803aa89600) + 82 at VifCoveringVnode.cpp:79
frame #4: 0xffffff7fae56c399 FileSystem-Isolation`VifVnopArgs<vnop_getattr_args_class>::getInputArguments(this=0xffffff80c5da3550, apWrapper=0xffffff80c5da35a0) + 201 at VifCoveringVnode.h:508
frame #5: 0xffffff7fae56c0b5 FileSystem-Isolation`VifFsdGetattrHook(ap=0xffffff80c5da35e0) + 53 at VNodeHook.cpp:1742
frame #6: 0xffffff802bf71c6b kernel`vnode_getattr [inlined] VNOP_GETATTR(vp=<unavailable>, vap=<unavailable>, ctx=<unavailable>) + 44 at kpi_vfs.c:3110
frame #7: 0xffffff802bf71c3f kernel`vnode_getattr(vp=0xffffff803a7932d0, vap=0xffffff80c5da3858, ctx=0xffffff8038c1a530) + 79 at kpi_vfs.c:2213
frame #8: 0xffffff802bf27c42 kernel`getattrlist_internal(ctx=0xffffff8038c1a530, vp=0xffffff803a7932d0, alp=0xffffff80c5da3f28, attributeBuffer=<unavailable>, bufferSize=<unavailable>, options=5, segflg=<unavailable>, alt_name=0x0000000000000000) + 1218 at vfs_attrlist.c:2660
frame #9: 0xffffff802bf2a011 kernel`getattrlistat_internal(ctx=0xffffff8038c1a530, path=<unavailable>, alp=0xffffff80c5da3f28, attributeBuffer=4335917968, bufferSize=920, options=5, segflg=<unavailable>, pathsegflg=<unavailable>, fd=<unavailable>) + 353 at vfs_attrlist.c:2762
frame #10: 0xffffff802bf23227 kernel`getattrlist(p=<unavailable>, uap=<unavailable>, retval=<unavailable>) + 119 at vfs_attrlist.c:2787
frame #11: 0xffffff802c24dcb2 kernel`unix_syscall64(state=0xffffff8038c38640) + 610 at systemcalls.c:366
When a first read or write request is received a sparse file is created to support data flow isolation for a file
frame #1: 0xffffff7f83359994 FileSystem-Isolation`VifCoveringFsd::createIsolationSparseFile(this=0xffffff801444ca20, coveredVnode=0xffffff809a9abd38, vfsContext=0xffffff80144ea440) + 1764 at VifCoveringVnode.cpp:788
frame #2: 0xffffff7f8335ac81 FileSystem-Isolation`VifCoveringFsd::rwData(this=0xffffff801444ca20, coveredVnode=0xffffff809a9abd38, args=0xffffff809a9abca0) + 801 at VifCoveringVnode.cpp:1183
frame #3: 0xffffff7f8335dfdc FileSystem-Isolation`VifCoveringFsd::processRead(this=0xffffff801444ca20, coverdVnode=0xffffff809a9abd38, ap=0xffffff809a9abde8, result=0xffffff809a9abd28) + 1756 at VifCoveringVnode.cpp:2411
frame #4: 0xffffff7f83376208 FileSystem-Isolation`VifFsdReadHook(ap=0xffffff809a9abde8) + 296 at VNodeHook.cpp:835
frame #5: 0xffffff8000d690d1 kernel`vn_read(fp=0xffffff8013c92660, uio=0xffffff809a9abe70, flags=<unavailable>, ctx=0xffffff809a9abf10) + 529 at kpi_vfs.c:3246
frame #6: 0xffffff8000feebba kernel`dofileread(ctx=0xffffff809a9abf10, fp=0xffffff8013c92660, bufp=4376717600, nbyte=8, offset=<unavailable>, flags=0, retval=<unavailable>) + 282 at kern_descrip.c:5615
frame #7: 0xffffff8000fee923 kernel`read_nocancel(p=0xffffff8010a77000, uap=0xffffff80111ef800, retval=<unavailable>) + 115 at sys_generic.c:213
frame #8: 0xffffff800104dcb2 kernel`unix_syscall64(state=0xffffff8010f1d700) + 610 at systemcalls.c:366
The sparse file related data structures are released when a vnode is being reclaimed. The data in a sparse file is left intact and can be used later if required.
frame #3: 0xffffff7fa997ff34 FileSystem-Isolation`VifSparseFile::freeDataFile(this=0xffffff803bdbf100) + 356 at VifSparseFile.cpp:866
frame #4: 0xffffff7fa997f422 FileSystem-Isolation`VifSparseFile::free(this=0xffffff803bdbf100) + 1266 at VifSparseFile.cpp:378
frame #5: 0xffffff7fa995a7b8 FileSystem-Isolation`VifCoveringFsd::VifReclaimCoveringVnode(this=0xffffff803221c560, coveringVnode=0xffffff803bdbe4b0) + 856 at VifCoveringVnode.cpp:1111
frame #6: 0xffffff7fa9976458 FileSystem-Isolation`VifFsdReclaimHook(ap=0xffffff80c0413d00) + 264 at VNodeHook.cpp:756
frame #7: 0xffffff802734b870 kernel`vclean(vp=0xffffff803bdbe4b0, flags=<unavailable>) + 656 at kpi_vfs.c:4799
frame #8: 0xffffff802734b35a kernel`vnode_reclaim_internal [inlined] vgone(vp=0xffffff803bdbe4b0, flags=<unavailable>) + 15 at vfs_subr.c:2429
frame #9: 0xffffff802734b34b kernel`vnode_reclaim_internal(vp=0xffffff803bdbe4b0, locked=<unavailable>, reuse=<unavailable>, flags=<unavailable>) + 251 at vfs_subr.c:4597
frame #10: 0xffffff8027345e49 kernel`vnode_put_locked(vp=0xffffff803bdbe4b0) + 185 at vfs_subr.c:4334
frame #11: 0xffffff8027368521 kernel`vn_closefile(fg=<unavailable>, ctx=0xffffff80c0413e60) + 177 at vfs_subr.c:4289
frame #12: 0xffffff80275b1680 kernel`closef_locked [inlined] fo_close(fg=0xffffff8037b81c20, ctx=0xffffff8034c4d950) + 14 at kern_descrip.c:5711
frame #13: 0xffffff80275b1672 kernel`closef_locked(fp=<unavailable>, fg=0xffffff8037b81c20, p=0xffffff8037de3960) + 354 at kern_descrip.c:4982
frame #14: 0xffffff80275ad88e kernel`close_internal_locked(p=0xffffff8037de3960, fd=<unavailable>, fp=0xffffff80389b5240, flags=<unavailable>) + 542 at kern_descrip.c:2765
frame #15: 0xffffff80275b13d6 kernel`close_nocancel(p=0xffffff8037de3960, uap=<unavailable>, retval=<unavailable>) + 342 at kern_descrip.c:2666
frame #16: 0xffffff802764dcb2 kernel`unix_syscall64(state=0xffffff80348f5f80) + 610 at systemcalls.c:366
The current implementation of a sparse file associates a timestamp with each data block and flushes data up to a provided timestamp to original file on underlying file system when VifSparseFile::flushUpToTimeStamp is called. The timestamp is a counter and has nothing to do with system or wall clocks.
errno_t VifSparseFile::flushUpToTimeStamp( __in SInt64 timeStamp,
__in vnode_t coveredVnode,
__in off_t fileDataSize,
__in vfs_context_t vfsContext );
If gFlushDataForTest is set to TRUE the data will be flushed synchronously for each write, this is a test option as name suggests. Overwise all written data will be retained in a sparse file until flushUpToTimeStamp is called. It is responsibility of an application that employs this isolation filter to decide when and how to flush data from a sparse file to original location.
Below are call stacks for read and write requests
thread #6: tid = 0x191c, 0xffffff7faa776c9a FileSystem-Isolation`VifSparseFile::performIO(this=0xffffff8041c10100, descriptors=0xffffff8033583500, dscrCount=1) + 26 at VifSparseFile.cpp:4379, name = '0xffffff8035f8e130', queue = '0x0', stop reason = breakpoint 1.1
frame #0: 0xffffff7faa776c9a FileSystem-Isolation`VifSparseFile::performIO(this=0xffffff8041c10100, descriptors=0xffffff8033583500, dscrCount=1) + 26 at VifSparseFile.cpp:4379
frame #1: 0xffffff7faa759488 FileSystem-Isolation`VifCoveringFsd::rwData(this=0xffffff803d959dc0, coveredVnode=0xffffff80d67ebd38, args=0xffffff80d67ebca0) + 2184 at VifCoveringVnode.cpp:1454
frame #2: 0xffffff7faa75acb7 FileSystem-Isolation`VifCoveringFsd::processRead(this=0xffffff803d959dc0, coverdVnode=0xffffff80d67ebd38, ap=0xffffff80d67ebde8, result=0xffffff80d67ebd28) + 1079 at VifCoveringVnode.cpp:2411
frame #3: 0xffffff7faa769a4f FileSystem-Isolation`VifFsdReadHook(ap=0xffffff80d67ebde8) + 159 at VNodeHook.cpp:835
frame #4: 0xffffff80281690d1 kernel`vn_read(fp=0xffffff80348a4d50, uio=0xffffff80d67ebe70, flags=<unavailable>, ctx=0xffffff80d67ebf10) + 529 at kpi_vfs.c:3246
frame #5: 0xffffff80283eebba kernel`dofileread(ctx=0xffffff80d67ebf10, fp=0xffffff80348a4d50, bufp=4558647984, nbyte=335, offset=<unavailable>, flags=1, retval=<unavailable>) + 282 at kern_descrip.c:5615
frame #6: 0xffffff80283eed32 kernel`pread_nocancel(p=0xffffff80340bf4b0, uap=0xffffff8035f4d3c0, retval=0xffffff8035f4d400) + 130 at sys_generic.c:252
frame #7: 0xffffff802844dcb2 kernel`unix_syscall64(state=0xffffff8035f36760) + 610 at systemcalls.c:366
thread #5: tid = 0x18ae, 0xffffff7faa776c9a FileSystem-Isolation`VifSparseFile::performIO(this=0xffffff8040b5b600, descriptors=0xffffff803356f0c0, dscrCount=1) + 26 at VifSparseFile.cpp:4379, name = '0xffffff8034d54c88', queue = '0x0', stop reason = breakpoint 1.1
frame #0: 0xffffff7faa776c9a FileSystem-Isolation`VifSparseFile::performIO(this=0xffffff8040b5b600, descriptors=0xffffff803356f0c0, dscrCount=1) + 26 at VifSparseFile.cpp:4379
frame #1: 0xffffff7faa759488 FileSystem-Isolation`VifCoveringFsd::rwData(this=0xffffff803d959dc0, coveredVnode=0xffffff80c1c9bce0, args=0xffffff80c1c9bc50) + 2184 at VifCoveringVnode.cpp:1454
frame #2: 0xffffff7faa75a782 FileSystem-Isolation`VifCoveringFsd::processWrite(this=0xffffff803d959dc0, coverdVnode=0xffffff80c1c9bce0, ap=0xffffff80c1c9bd68, result=0xffffff80c1c9bcd8) + 1282 at VifCoveringVnode.cpp:2169
frame #3: 0xffffff7faa769dfb FileSystem-Isolation`VifFsdWriteHook(ap=0xffffff80c1c9bd68) + 171 at VNodeHook.cpp:940
frame #4: 0xffffff8028173700 kernel`VNOP_WRITE(vp=0xffffff8040b88b40, uio=0xffffff80c1c9be70, ioflag=<unavailable>, ctx=<unavailable>) + 112 at kpi_vfs.c:3287
frame #5: 0xffffff8028168c5f kernel`vn_write(fp=0xffffff80348a4a68, uio=0xffffff80c1c9be70, flags=0, ctx=0xffffff80c1c9bf10) + 895 at vfs_vnops.c:1112
frame #6: 0xffffff80283ef305 kernel`dofilewrite(ctx=0xffffff80c1c9bf10, fp=0xffffff80348a4a68, bufp=140518634042512, nbyte=348, offset=<unavailable>, flags=<unavailable>, retval=<unavailable>) + 309 at kern_descrip.c:5636
frame #7: 0xffffff80283ef152 kernel`write_nocancel(p=0xffffff80340271a0, uap=<unavailable>, retval=0xffffff8035e4cb60) + 274 at sys_generic.c:476
frame #8: 0xffffff802844dcb2 kernel`unix_syscall64(state=0xffffff8035f34700) + 610 at systemcalls.c:366
To activate the isolation layer just load the kernel extension (kext) with the kextload command. For testing purposes the isolation layer intercepts requests to files on a removable drive mounted at /Volumes/Untitled . Sparse files are created in /work/isolation directory which must exist before the kextd is being loaded. These test settings are defined as
const static char* TestPathPrefix = "/Volumes/Untitled/";
...
#define TEST_ISOLATION_DIR "/work/isolation/"
#define TEST_ISOLATION_FILE_NAME TEST_ISOLATION_DIR"XXXXXXXX_XXXXXXXX_XXXXXXXX_XXXXXXXX_AAAAAAAA"