5

macOS Catalina introduces "firmlinks" to the APFS filesystem. Is there a way via a CLI tool to determine whether /Users is a firmlink and where it is firmlinking to? I'm looking for something similar to the readlink command for symlinks.

3 Answers 3

3

A list of the system firmlinks can be found in the file /usr/share/firmlinks.

On macOS Catalina version 10.15.1, the contents of the firmlinks file appear as follows:

/AppleInternal  AppleInternal
/Applications   Applications
/Library        Library
/System/Library/Caches  System/Library/Caches
/System/Library/Assets  System/Library/Assets
/System/Library/PreinstalledAssets      System/Library/PreinstalledAssets
/System/Library/AssetsV2        System/Library/AssetsV2
/System/Library/PreinstalledAssetsV2    System/Library/PreinstalledAssetsV2
/System/Library/CoreServices/CoreTypes.bundle/Contents/Library  System/Library/CoreServices/CoreTypes.bundle/Contents/Library
/System/Library/Speech  System/Library/Speech
/Users  Users
/Volumes        Volumes
/cores  cores
/opt    opt
/private        private
/usr/local      usr/local
/usr/libexec/cups       usr/libexec/cups
/usr/share/snmp usr/share/snmp
2

I'm also looking for this. In the meantime, I've resorted to using comparing the results of pwd -P (show physical path of process working directory) with that of df ., e.g.:

# cd /Users ; pwd -P ; df .; cd - >/dev/null
/Users
Filesystem   512-blocks       Used Available Capacity iused      ifree %iused  Mounted on
/dev/disk1s1 1953595632 1384119832 545065168    72% 1287392 9766690768    0%   /System/Volumes/Data

(The final cd - switches back to the original directory, and prints out the directory path, hence the redirection to /dev/null.)

Note how the output of pwd -P says /Users, while df says it is mounted on /System/Volumes/Data.

0

Note: the answer below is a partial non-answer, because there is something wrong with the firmlink flag in macOS or in stat. (An explanation or solution would be most welcome.)

XNU's /sys/stat.h has a flag for firmlinks (SF_FIRMLINK). Normally you would check for a flag by reading the decimal output of all user flags with the stat command, converting it to hexadecimal, and testing bitwise against the hexadecimal value of the respective flag.

Example for Applications, testing for SF_NOUNLINK (0x00100000):

filepath="/Applications"
flag_hex="0x00100000"
st_flags=$(printf '0x%x' $(stat -f %f "$filepath"))
[[ $((( $(echo "$st_flags") & $flag_hex ))) -eq 0 ]] && echo "Flag $flag_hex is *not* set" || echo "Flag $flag_hex is set"

This will print that Flag 0x00100000 is set.

Conversely, testing for an unset flag, e.g. UF_HIDDEN (0x00008000), would result in a negative, because in our case /Applications is not a hidden file.

filepath="/Applications"
flag_hex="0x00008000"
st_flags=$(printf '0x%x' $(stat -f %f "$filepath"))
[[ $((( $(echo "$st_flags") & $flag_hex ))) -eq 0 ]] && echo "Flag $flag_hex is *not* set" || echo "Flag $flag_hex is set"

The problems, however, begin with firmlinks. Let's test for the firmlink flag of /Applications:

filepath="/Applications"
flag_hex="0x00800000"
st_flags=$(printf '0x%x' $(stat -f %f "$filepath"))
[[ $((( $(echo "$st_flags") & $flag_hex ))) -eq 0 ]] && echo "Flag $flag_hex is *not* set" || echo "Flag $flag_hex is set"

This should output Flag 0x00800000 is set, but it does not. Instead it tells us that the SF_FIRMLINK flag is not set, even though /Applications is objectively a firmlink.

When you just look at the variable $st_flags

filepath="/Applications"
st_flags=$(printf '0x%x' $(stat -f %f "$filepath"))
echo "$st_flags"

…then the output is just 0x100000 (SF_NOUNLINK only), but not 0x900000 (combined SF_NOUNLINK and SF_FIRMLINK).

This means that either macOS is not writing this flag at all, even though it's defined in /sys/stat.h, or (more likely) Apple have deliberately kept this flag out of stat, lstat etc.

Either way, for the command-line it currently seems that we can only base a solution on Hambly's answer, unless someone knows or can create a tool that is somehow able to read these file flags without using stat/lstat. You would probably have to apply methods like getattrlist(), which according to its man page is like a "seriously enhanced version of stat(2)".

For the command-line you probably should also take synthetic firmlinks into account. So a quick solution (firmlink yes|no) would look e.g. like this:

firmlinks=$(awk '{print $1}' < /usr/share/firmlinks)
synthetics=$(grep -v -e "^$" -e "\#" < "/etc/synthetic.conf" | awk '{print "/"$1}')
firmlinks=$(echo -e "$firmlinks\n$synthetics" | grep -v "^$" | sort)
for filepath in "$@"
do
    filepath=$(echo "$filepath" | sed 's-/$--')
    echo "$firmlinks" | grep -q "^$filepath$" &>/dev/null && echo "Firmlink: $filepath"
done

The following would be a proper solution to also print the actual link information e.g. Firmlink: /Users -> Users

firmlinks=$(cat /usr/share/firmlinks 2>/dev/null)
synthetics=$(grep -v -e "^$" -e "\#" < "/etc/synthetic.conf" 2>/dev/null | sed 's-^-/-g')
firmlinks=$(echo -e "$firmlinks\n$synthetics" | grep -v "^$" | sort)
for filepath in "$@"
do
    filepath=$(echo "$filepath/" | sed 's-/*$--')
    firmlink_raw=$(echo "$firmlinks" | grep "^$filepath\t")
    if [[ $firmlink_raw ]] ; then
        firmlink1=$(echo "$firmlink_raw" | awk -F\t '{print $1}')
        firmlink2=$(echo "$firmlink_raw" | awk -F\t '{print $2}')
        echo "Firmlink: $firmlink1 -> $firmlink2"
    fi
done

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .