defaults write with double slash

published 30 Jan 2018

My ass has been bitten twice by this now, so I figured a blog post to warn others might be in order. Say you’re writing a package postinstall script where you want to set a preference, and you dutifully use $3 to target the installation volume. You might write a script along these lines:

#!/bin/bash
# This is buggy, don't use it. A working script is at the bottom of this post.

defaults write "$3/Library/Preferences/com.tyrell.nexussix" lifespan -int 4

Next thing you know some of your replicants grow old and decide to kill you, as when an app tries to read the preference it gets an unexpected result:

#!/usr/bin/python

from __future__ import unicode_literals
from __future__ import print_function
from Foundation import CFPreferencesCopyAppValue

print(CFPreferencesCopyAppValue("lifespan", "com.tyrell.nexussix")) # sometimes None, sometimes 4

Yet if we look with the defaults command, everything seems fine:

# defaults read /Library/Preferences/com.tyrell.nexussix
{
    lifespan = 4;
}

To make things even more confusing, CFPreferences suddenly sees the value now. To untangle this mess, let’s investigate what happens in our postinstall script when we’re installing the package on the current system, with $3 expanded to /:

# defaults write //Library/Preferences/com.tyrell.nexussix lifespan -int 4
# defaults write /Library/Preferences/com.tyrell.nexusseven lifespan -int 4
# ls -lf /Library/Preferences/com.tyrell.nexus*
-rw-------  1 root  wheel  57 30 Jan 12:38 /Library/Preferences/com.tyrell.nexussix.plist
-rw-r--r--  1 root  wheel  57 30 Jan 12:38 /Library/Preferences/com.tyrell.nexusseven.plist

The extra slash causes defaults to make the preference file readable only by root, making it sometimes visible through cfprefsd, depending on access permissions and caching. My guess is the slash triggers defaults’ codepath for writing to user libraries, where preference files shouldn’t be readable by other users.

So how should we be writing our postinstall scripts? I’m glad you asked. I’ve been using the following in my scripts:

#!/bin/bash

if [[ "$3" == "/" ]]; then
    TARGET=""
else
    TARGET="$3"
fi

defaults write "$TARGET/Library/Preferences/com.tyrell.nexussix" lifespan -int 4

So what happens if you install this package on a system image with AutoDMG? Well the pref plist will only be readable by root, but as far as I can tell apps can still access their preference keys, presumably since cfprefsd caches everything when the system boots — at least I haven’t been able to reproduce the problem this way.

References: