FSKit after mount -F: "ls: fts_read: Operation not permitted"

·4 mins

My new FSKit filesystem mounted without complaint, but nothing inside it could be read:

$ ls -la ~/mnt/storage-test
ls: fts_read: Operation not permitted
total 0
$ cat ~/mnt/storage-test/sub/a.txt
cat: /Users/me/mnt/storage-test/sub/a.txt: Operation not permitted

stat on the mount point worked. If your FSKit extension does the same after a mount -F from the command line, check its sandbox before your code: the extension can see the source folder’s metadata but may not read its contents.

The setup

  • macOS 26, a file system extension built on FSKit’s FSUnaryFileSystem that exposes a local folder through a mount point
  • A small host app, StorageHost.app, in /Applications; its executable runs mount -F -t storagefs <source> <mount point>
  • The extension StorageFS.appex with the FSKit entitlement and the app sandbox
  • Info.plist with FSSupportsPathURLs set to true and FSRequiresSecurityScopedPathURLResources set to false

What went wrong

The first mount was over ~/Downloads, so the obvious suspect was privacy protection (TCC) for that folder. A fresh ~/storage-test with one file in it failed the same way, which ruled TCC out and pushed the search elsewhere.

My coding agent’s first log queries came back empty, and it told me there were no log entries. That was wrong. In zsh, log is a builtin, so log show ... 2>/dev/null runs the builtin, sends its too many arguments error to /dev/null, and prints nothing. Call /usr/bin/log by its full path. With that, the kernel had the answer:

kernel (Sandbox) 3 duplicate reports for Sandbox: StorageFS(83505) deny(1) file-read-data /Users/me/storage-test

The extension’s own log showed the failures from FSKit’s side: readDirectory:...reply:error:1 and readFrom:...reply:error:1. Error 1 is EPERM, which ls turns into fts_read: Operation not permitted.

Fix

mount -F gives the extension a plain path, not a security-scoped URL, so a sandboxed process can’t read an arbitrary path. For development, grant temporary sandbox exceptions in the extension’s .entitlements file:

<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
	<string>/</string>
</array>
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
	<string>/Volumes/</string>
</array>

This covers your home folder and external drives. It doesn’t override privacy protection, so ~/Downloads, ~/Documents, and ~/Desktop may still be refused; test with a folder elsewhere. Temporary exceptions are for development on your own machine. A shipping extension should use security-scoped resources instead. FSRequiresSecurityScopedPathURLResources in Info.plist requests that; I haven’t managed a clean test yet (see below).

The same logs flagged two more problems that I fixed along the way:

-[FSModuleVolume(Project) getMaxFileSizeInBits]: Volume implements neither maxFileSizeInBits nor maxFileSize. One of them must be implemented.
-[FSVolumeConnector getStandardItemAttributesForItem:replyHandler:]_block_invoke: Reported attributes are incomplete

The first needs maximumFileSizeInBits on the volume. The second went away once the item attributes set flags, which cleared the log entry.

Reinstalling switches the module off

To test each change, I replaced the app: rm -rf /Applications/StorageHost.app, copied the new build, and launched it once. After that, mount failed outright:

Module com.example.storage.fs is disabled!
mount: Unable to invoke task

pluginkit disagreed. pluginkit -e use -i com.example.storage.fs succeeded, and pluginkit -m listed the extension with a +, which means enabled. FSKit still refused. It keeps its own switch in System Settings → General → Login Items & Extensions → File System Extensions, and I didn’t find a command-line way to flip it. After I turned StorageFS back on there, the next mount listed and read files, and appends, renames, truncation, and an 8 MB write-and-readback all worked.

The disabled module also spoiled my one test of security-scoped resources. With FSRequiresSecurityScopedPathURLResources set to true, mount failed with Unable to invoke task, but the reinstall had just switched the module off, so the failure says nothing about the setting.

Why

An FSKit module is an app extension, and FSKit requires it to run in the app sandbox. When an app hands a folder to the extension, sandbox access can travel with it as a security-scoped URL. mount -F on the command line passes only a string. The sandbox lets the extension look up metadata for that path, which is why the mount and stat worked, and denies file-read-data, which every directory listing and file read needs.

Takeaway

If an FSKit mount works and every read returns Operation not permitted, search the kernel log for deny(1) file-read-data with /usr/bin/log, then give the extension sandbox access to the source path. After every reinstall, check the File System Extensions switch before you trust a test result.

If you’ve got FSRequiresSecurityScopedPathURLResources working with a command-line mount -F, I’d like to hear how.