Rixstep
 About | ACP | Buy | Industry Watch | Learning Curve | News | Products | Search | Substack
Home » Learning Curve

App Cleaners Revisited

There's always one app.


Get It

Try It

There's always one app that's the rage at any given time on any platform. Things get blown out of proportion pretty fast. Everybody and their cousins try to whip something up fast and rush it out the door. Users suffer.

That app used to be the file shredder on Windows. Then someone discovered that some of the shredders didn't shred anything at all, but only 'unlinked' the files, so you couldn't find them but the FBI could. Then the app was the memory optimiser. But that too turned out to be snake oil, essentially and effectively doing nothing at all, with some variations actually containing a disclaimer to that effect. Then the app was the disk optimiser. Something OS X file systems do pretty well on their own.

Snake oil.

Apple Didn't Forget Shit

A big one on OS X has been the 'app cleaner'. And all too many vendors include the tag 'the app Apple forgot'. For once and for all and make no mistake about it: Apple didn't forget shit.

No one knows OS X like the Apple engineers. They've got the source code! If there was a way to remove all vestiges of an application, they'd have provided it long ago. And as pointed out at this site on more than one occasion: what about the app installer? Who's going to counteract what that program does? None of the 'app cleaners' out there even think of that.

Adding insult to injury, the invariable comments on software pages are to the effect 'oh I tried so-and-so and it's so much better!' How the F do they know? Clue: they don't. It's all nonsense.

There's only one app out there that truly finds everything apps leave behind and it's Tracker. Tracker isn't 'heuristic' like the snake oil apps. It relies purely on file system intrinsics. In other words: what happened between time A and time B. And it follows all changes through all directories, even if they're closed off to you the user. Tracker finds everything.

Unix File Systems

Unix file systems have three very important timestamps in this context.

  1. Last accessed. Every time you open a file, the file system driver makes a note of it and bumps this field accordingly.
  2. Last modified. This field gets bumped when you actually write to a file. Again, the file system driver takes care of matters.
  3. Last changed. This 'changed' refers not to the file itself but to the meta data related to the file. Things like file size, ownership - and yes, the timestamps too.

That last point is very instructive. Intelligent malware might try to disguise operations by wiping away the 'last modified' timestamp and replacing it with the previous one. That won't work. For although it's possible to programmatically set the 'last accessed' and 'last modified' timestamps, you can't do that with the 'last changed' stamp. That's the dominion of the drivers alone. Should anything happen to the timestamps, this field gets bumped, and there's nothing malware can do about it.


The standard ACP file info sheet and the special Tracker info sheet. Tracking began at 21:28:19 Wednesday 4 February 2009 and the file 'com.apple.LaunchServices-023501.css' was flagged because it was accessed, modified, and 'changed' 29 seconds later.

So a proper tracker, assigned the task of noting all changes to a file system for any given period of time or any given run of any given application, will be able to see:

  1. What files the application merely accessed. Trojans like to phone home with the results of data mining expeditions.
  2. What files the application modified. For obvious reasons.
  3. What files the application tried to hide the truth about. The 'changed' field.
  4. What other applications/processes were launched and what they did. Same as above.

Such a tracking system (as used by Tracker) is the only complete, only foolproof way to see what an application - or what anything - is up to on disk. But that's not the way the 'app cleaners' work.

AppTrap

AppTrap is one of a myriad 'app cleaners' for OS X available today. Some of these app cleaners have as many as 800,000 downloads at CNET. The comments show basically that none of the users have a clue what they're doing.

AppTrap differs in two ways from your run of the mill snake oil.

  1. It doesn't tell you what it finds and deletes.
  2. It's open source and is available on Git Hub.

AppTrap installs itself as a pane in System Preferences. So it runs all the time in the background, sucking CPU clocks whenever it gets a notification that something (perhaps of importance, perhaps not) is happening to the file system.

But that's not what's important here. What's important is how AppTrap works - how all 'app cleaners' work. And now there's source code to prove it.

AppTrap was originally written by Markus Magnuson, but the code has since passed into other hands. And it's open source, as mentioned before. This gives us an excellent opportunity to see what's actually being done behind the scenes. There should be no surprises for IT pros, but for the unwashed, this is an excellent opportunity to inspect the 'app cleaner' variety of application without actually being harmed by it.

The actual source code will be presented here, along with non-technical explanations of what exactly is going on. The code has been cleaned up a bit for readability and efficiency.

Here we go.

ATApplicationController

The guts of AppTrap is in the ATApplicationController module - more specifically the file ATApplicationController.m. This file is the only file we need look at.

Let's start at the beginning.

-----------------------------------------------
  APPTRAP LICENSE

  "Do what you want to do,
  and go where you're going to
  Think for yourself,
  'cause I won't be there with you"

  You are completely free to do anything with
  this source code, but if you try to make
  money on it you will be beaten up with a
  large stick. I take no responsibility for
  anything, and this license text must
  always be included.

  Markus Amalthea Magnuson <markus.magnuson@gmail.com>
-----------------------------------------------
*/

That's the prologue. It's free code. With certain caveats.

@implementation ATApplicationController

That indicates the code for ATApplicationController is about to begin.

-(id)init {
    if ((self = [super init])) {
    // Setup the path to the trash folder
    pathToTrash = nil;
    CFURLRef trashURL;
    FSRef trashFolderRef;
    OSErr err;

This is an instance method. AppTrap should run only one instance, but this standard hook is used anyway. Here the app's trying to find the canonical path to the Trash. This presumably because that's where files will be put when they're deleted.

// Setup paths for application folders
applicationsPaths = [[NSSet alloc] initWithArray:
        NSSearchPathForDirectoriesInDomains(NSAllApplicationsDirectory, NSLocalDomainMask | NSUserDomainMask, YES)];

What AppTrap is trying to do here is find the places where applications are normally found. Rixstep's Lightman has an excellent facility for finding these paths. They're controlled by APIs and not baked into the system per se. These are paths the system itself uses for searches.

But there's no guarantee that users will only use the 'official' paths. Applications can be placed anywhere in a file system. AppTrap will most likely miss such applications - but again: that's not germane to the discussion.

// Setup paths for library items, where we'll search for files
NSMutableArray *tempArray = [[NSMutableArray alloc] init];
NSArray *tempSearchArray = nil;
NSEnumerator *e = nil;
id currentObject = nil;

// Preferences and StartupItems
tempSearchArray = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask | NSLocalDomainMask, YES);

AppTrap will try to find 'ancillary files' connected with application you want 'cleaned'. This is key - this is how the application goes about finding them. It doesn't check the file system to see what files have been generated etc - it only looks in standard locations. And if your trusty app decides to put things elsewhere, they won't be found.

// Application Support
[tempArray addObjectsFromArray:NSSearchPathForDirectoriesInDomains(
        NSApplicationSupportDirectory, NSUserDomainMask | NSLocalDomainMask, YES)];

'Application Support' is another place applications put files. Tshis places the files external to the application bundle which can get updated at any time, and given how Apple file systems work, support files in the bundle would be obliterated.

// Cache
[tempArray addObjectsFromArray:NSSearchPathForDirectoriesInDomains(
        NSCachesDirectory, NSUserDomainMask | NSLocalDomainMask, YES)];

Yes there's a cache directory in ~/Library too. All this is good to check.

// Register for changes to the trash
[self registerForWriteNotifications];

A key line. What AppTrap will do is wait for you to 'trash' an app you no longer want. The file system will notify AppTrap whenever you do this. The bad thing is this applies to everything that goes into the trash, not just apps you want gone. And remember: you never get told what happens behind the scenes.

Note the above method refers to the app's own code. Embedded in the app is code from Uli Kusterer - the UKKQueue module that actually watches changes in the file system.

Another mystery of course is why the app didn't simply use NSWorkspaceDidPerformFileOperationNotification. But it's not germane. The app registers with the NSWorkspace notification centre and seems to redirect notifications to the Kusterer module.

// Return all applications currently in the trash, as an array
// TODO: Can this method be incorporated in handleWriteNotification: to speed things up?
-(NSArray *)applicationsInTrash {

This is what the app's going to do when something turns up in the trash. This is actually not germane, but let's take a look anyway.

NSMutableArray *applicationsInTrash = [NSMutableArray array];

He's going to check all the files in the trash, and from that make a list of all the applications there.

while ((currentFilename = [e nextObject])) {
    if ([currentFilename hasSuffix:@".app"])
        [applicationsInTrash addObject:currentFilename];

Quite straightforward. While there are still further files to check, if the current file being checked has the extension 'app', put it in the list. But why the code doesn't use the more correct pathExtension method is yet another mystery.

-(void)handleWriteNotification:(NSNotification *)notification {

What to do when the notification system says there's been a disk write operation.

while ((currentFilename = [e nextObject])) {
    // Is it on the whitelist?
    if ([whitelist containsObject:currentFilename])
        continue;

There's a 'white list' of apps you never touch. If the one you're checking is on that list, ignore it.

// If it's in the applications folder, it was probably auto-updated by Sparkle

Ah. We're actually checking in those 'applications' directories we found earlier. And one of the 'sparkly' add-ons might be doing some nasty work. So ignore.

// XXX: Currently only works for applications in root (not apps in folders), we could of course recurse
// with an NSDirectoryEnumerator but that would be _reeeally_ slow since this method is called very often

Basically an admission that this method of cleaning apps sucks!

while ((currentApplicationPath = [applicationPathsEnumerator nextObject]))
    if ([manager fileExistsAtPath:[currentApplicationPath stringByAppendingPathComponent:currentFilename]])
        // Add it to the whitelist
        [whitelist addObject:currentFilename];

He now goes about updating his white list. Comments anyone?

// Now, check again for safety
if ([whitelist containsObject:currentFilename])
    continue;

Same thing as before. Unfortunately.

NSLog(@"I just trapped the application %@!", currentFilename);

Yay! Ahem.

// Get the full path of the trapped application
NSString *fullPath = [pathToTrash stringByAppendingPathComponent:currentFilename];

Now we caught one. Now we have to find what else could be on disk. Behold the logic of the 'app cleaner'.

// Get the applications's bundle and its identifier
NSBundle *appBundle = [NSBundle bundleWithPath:fullPath];
NSString *preferenceFileName = [[appBundle bundleIdentifier] stringByAppendingPathExtension:@"plist"];
NSString *preflockFileName = [preferenceFileName stringByAppendingPathExtension:@"lockfile"];

It's thorough at any rate. And it's been updated for 10.7 and above where those nasty 'lockfiles' start to appear. He's looking for the name of the app's preferences file and from that he's going to find the full path for the lockfile.

NSString *lssflprefFileName = [[appBundle bundleIdentifier] stringByAppendingPathExtension:@"LSSharedFileList.plist"];
NSString *lssflpreflocklFileName = [lssflprefFileName stringByAppendingPathExtension:@"lockfile"];

Now he gets the other two pieces of junk Apple leave behind in ~/Library/Preferences these days.

But note: so far the only place we've looked is ~/Library/Preferences. What if this app had done something in stealth? Yes there are 'bona fide' apps out there that do things like this. There are mainstream apps that actually hide data in other people's preferences files! There are mainstream apps that hide stuff in the system's global preferences files! How the F are you supposed to find that? You can't - not with your 'app cleaner'.

// Let's find some system files
NSMutableSet *matches = [[NSMutableSet alloc] init];
NSEnumerator *libraryEnumerator = [libraryPaths objectEnumerator];
id currentLibraryPath = nil;

Let's find some system files! Yes let's do that! Wahoo!

while ((currentLibraryPath = [libraryEnumerator nextObject])) {
    [matches addObjectsFromArray:[self matchesForFilename:preferenceFileName atPath:currentLibraryPath]];
    [matches addObjectsFromArray:[self matchesForFilename:preflockFileName atPath:currentLibraryPath]];
    [matches addObjectsFromArray:[self matchesForFilename:lssflprefFileName atPath:currentLibraryPath]];
    [matches addObjectsFromArray:[self matchesForFilename:lssflpreflocklFileName atPath:currentLibraryPath]];
    [matches addObjectsFromArray:[self matchesForFilename:appName atPath:currentLibraryPath]];
}

What's all that? Looking for the four files mentioned plus the app itself.

The 'matchesForFilename' method seems to have a few issues.

// Part of code from http://www.borkware.com/quickies/single?id=130
// TODO: Seems like were leaking NSConcreteTask and NSConcretePipe here, needs to be investigated
-(NSArray *)matchesForFilename:(NSString *)filename atPath:(NSString *)path {

The code at the cited URL has the following warning.

One important note: don't feed in arbitrary user data using this trick, since they could sneak in extra commands you probably don't want to execute.

Meaning it might be possible to create a land mine that could exploit AppTrap to get arbitrary code to execute. Not good.

What's otherwise interesting about this strange critter is it uses a background console process to do file system searches. That's really funny.

// Open up the window if we got any hits
if ([[listController arrangedObjects] count] > 0) {
    [NSApp activateIgnoringOtherApps:YES];
    [NSApp runModalForWindow:mainWindow];
}

Show something onscreen if you've been a good hunter. But don't let the user choose which files to trash.

// Clear the whitelist if the trash is empty, i.e an "Empty trash"
// operation was just finished
if ([self numberOfVisibleItemsInTrash] == 0)
    [whitelist removeAllObjects];

Clear away your white list until next time (then access it twice, both before and after you rebuild it).

Bottom Line

That's about it. A final curious thing about AppTrap is that it uses three separate methods (code blocks) to react to the application launch, each requiring a separate system call. But whatever. The basic things we learned from the above are the following.

  1. App cleaners look only in designated places for files.
  2. Some of them don't look in many places or for many files at all.
  3. Some of them go through a lot of work and end up doing very little.
  4. App cleaners can't offer protection in most common scenarios.
  5. App cleaners can't protect you from dumb or malicious installers.
  6. App cleaners don't even try to find what's happened on disk.
  7. Most users of these app cleaners have no fucking clue.

More to Read

App Cleaner, AppDelete, AppZapper, CleanApp, and uApp were all tested previously. All failed miserably. This is a seven-page report: read it here.

See Also
Tracker Fact Sheet
Tracker: Why Chance It?

CleanMyMac 1.5.1
Tim Ferriss Facepalms
The OmniFocus Project
Cleaning Begins at Home
The App Remover Codicil
AppZapper 1.8.0: A Fairy Tale?

About | ACP | Buy | Industry Watch | Learning Curve | News | Products | Search | Substack
Copyright © Rixstep. All rights reserved.