Are read-only filesystems currently supported by FSKit?

I'm writing a read-only filesystem extension.

I see that the documentation for loadResource(resource:options:replyHandler:) claims that the --rdonly option is supported, which suggests that this should be possible. However, I have never seen this option provided to my filesystem extension, even if I return usableButLimited as a probe result (where it doesn't mount at all - FB19241327) or pass the -r or -o rdonly options to the mount(8) command. Instead I see those options on the volume's activate call.

But other than saving that "readonly" state (which, in my case, is always the case) and then throwing on all write-related calls I'm not sure how to actually mark the filesystem as "read-only." Without such an indicator, the user is still offered the option to do things like trash items in Finder (although of course those operations do not succeed since I throw an EROFS error in the relevant calls).

It also seems like the FSKit extensions that come with the system handle read-only strangely as well. For example, for a FAT32 filesystem, if I mount it like

mount -r -F -t msdos /dev/disk15s1 /tmp/mnt

Then it acts... weirdly. For example, Finder doesn't know that the volume is read-only, and lets me do some operations like making new folders, although they never actually get written to disk. Writing may or may not lead to errors and/or the change just disappearing immediately (or later), which is pretty much what I'm seeing in my own filesystem extension. If I remove the -F option (thus using the kernel extension version of msdos), this doesn't happen.

Are read-only filesystems currently supported by FSKit? The fact that extensions like Apple's own msdos also seem to act weirdly makes me think this is just a current FSKit limitation, although maybe I'm missing something. It's not necessarily a hard blocker given that I can prevent writes from happening in my FSKit module code (or, in my case, just not implement such features at all), but it does make for a strange experience.

(I reported this as FB21068845, although I'm mostly asking here because I'm not 100% sure this is not just me missing something.)

Answered by DTS Engineer in 866824022

Are read-only filesystems currently supported by FSKit?

I think the tricky part here is what "support" here actually means. Let me start by what this actually "does":

pass the -r or -o rdonly options to the mount(8) command.

Passing that to mount should mean that the VFS layer itself is prevented from writing to the device. In FSKit terms, that means "FSBlockDeviceResource.writable" should be false and that all write methods should fail. If either of those behave differently, then that's a HUGE bug that we'd need to fix ASAP.

However, the confusing point here is that mounting a volume "readonly" doesn't necessarily define/change how the file system "presents" itself to the higher level system. That is, strictly speaking, nothing prevents a volume being mounted "readonly"... but that file system itself allowing itself to be fully modifiable.

That might sound a bit strange, but as a concrete example, you could implement a "resettable" file system by using the on-disk file system as the starting structure, routing all writes to secondary storage, and then discarding that storage on unmount. That's just one example, but the broader point is that the data "source" and lower-level VFS system aren't designed to control/constrain the file system that's actually presented to the system.

That leads to here:

Then it acts... weirdly. For example, Finder doesn't know that the volume is read-only, and lets me do some operations like making new folders, although they never actually get written to disk.

So, FYI, this is what happens when the high-level system presents operations as "possible" when it can't actually perform those operations. You can actually get exactly the same thing to happen to any of our kernel drivers, though it takes a KEXT*.

*More specifically, having written a KEXT that did this earlier in my career, if the IOMedia driver (the top level of the IOKit storage stack) "flips" itself to read-only AFTER the volume has mounted, you basically get exactly the behavior you're describing.

In terms of what your extension should do (and why msdosfs is failing), I think the best approach here is to support permissions (FSItemAttributeMode), and then ALWAYS return a configuration for all objects that prevents writing and fail any attempt to modify mode. You could also implement FSVolumeAccessCheckOperations; however, I'm not sure that will actually behave the way you want, and the mode operation is the more important check. The mode configuration means that the system (particularly the Finder) "knows" that the object is read-only before it attempts any operation, which means it never bothers trying.

That leads to here:

It also seems like the FSKit extensions that come with the system handle read-only strangely as well. For example, for a FAT32 filesystem,

It also seems like the FSKit extensions that come with the system handle read-only strangely as well.

Please file a bug on this (specifically, msdosfs's behavior) and post the bug number once it's filed.

What's going on here is that we're returning FSVolumeSupportedCapabilities.doesNotSupportSettingFilePermissions because, in fact, FAT does not support file permissions. However, that also means that it can't "tell" the higher-level system that it's read-only because it's disabled file permissions. In any case, I think it could solve all this by removing "doesNotSupportSettingFilePermissions" on read-only volumes and using exactly the same approach I'm suggesting above... and if it can’t, then this is an edge case we should probably solve. Either way, it's worth a bug.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Accepted Answer

FB22267894 is fixed in macOS 26.5 beta 2. Now code like

@available(macOS 26.0, *)
var requestedMountOptions: FSVolume.MountOptions {
    get {
        return [.readOnly]
    }
    set {}
}

has the desired effect.


Also, as a follow-up question, is @available(macOS 26.0, *) safe in this case? I saw in the macOS 26.4 release notes that the availability version is wrong and actually 26.4, not 26.0 (171914656). However using @available(macOS 26.4, *) fails to compile [1], which seems kind of odd. Will that cause problems in macOS 26.0 through 26.3?

[1] with the error Protocol 'Operations' requires 'requestedMountOptions' to be available in application extensions for macOS 26.0 and newer

Also, as a follow-up question, is @available(macOS 26.0, *) safe in this case?

Yes, as I don't think you actually need to use an availability macro at all. The property itself ("requestedMountOptions") is just a new method your object "happens" to have implemented. On newer systems (which know/expect the property), the system will call it while older systems will just ignore it.

Similarly, I believe the typedef "FSVolume.MountOptions" and value ".readOnly" all end up getting stripped out at compile time, replaced by their underlying "raw" types and/or values.

[1] with the error Protocol 'Operations' requires 'requestedMountOptions' to be available in application extensions for macOS 26.0 and newer

Huh. Not sure why that's happening, as "requestedMountOptions" should be optional, so you don't need to implement it all.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I don't think you actually need to use an availability macro at all.

In this case there's a compile error if I don't include the macro as my minimum deployment target is macOS 15.6 and FSVolume.MountOptions is not an available type in that version.

Not sure why that's happening, as "requestedMountOptions" should be optional, so you don't need to implement it all.

Yeah, it was quite weird... I actually had a drafted FB about that compile error that I haven't cleaned up and submitted yet. I found some time to do that (FB22525990), though as long as putting 26.0 in the macro doesn't cause any weird or undefined behavior then it shouldn't be that big of a problem.

Thanks for helping out with this!

In this case, there's a compile error if I don't include the macro as my minimum deployment target is macOS 15.6, and FSVolume.MountOptions is not an available type in that version.

Yeah, I didn't think of that.

Yeah, it was quite weird...

Indeed, it is...

I actually spent a bit of time poking at this today and was only left more confused. I'm far from an expert in Swift, but I wasn't able to generate a new protocol that generates the same behavior that you're seeing. So, for example, this code builds fine:

@objc public protocol Foo {
    @available(iOS 26.0, *)
    @objc optional var requestedFoo: FSVolume.MountOptions { get set }
}

class SampleVolume: FSVolume, FSVolume.Operations, Foo {
...
    @available(iOS 26.4, *)
    var requestedFoo: FSVolume.MountOptions {
        get {
            logger.log("Reading requestedMountOptions")
            return [.readOnly]
        }
        set {}
    }

I'm going to poke Quinn about this and see what he says, but I'm more and more convinced that there is a weird compiler bug/quirk involved here.

Having said that:

though as long as putting 26.0 in the macro doesn't cause any weird or undefined behavior, then it shouldn't be that big of a problem.

...I also can't think of how this would ever generate any problems. The bottom line here is that older systems will ignore it, newer systems will call it, and either should basically work fine.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hmm. I tried something like the code you sent, and same as you, it compiled. But when I changed iOS to macOS in the availability macro (which I assume is what you intended to write), then I get the compile error... weird... this is also the case if I change FSVolume.MountOptions to something like Int, so it's not because the FSKit types are macOS only.

Here's some sample code that reproduces this (also added the code to my feedback):

@objc protocol SwiftProtocol {
    @available(macOS 26.0, *)
    @objc optional var requestedFoo: Int { get set }
}

class ReproduceBug: SwiftProtocol {
    @available(macOS 26.2, *)
    var requestedFoo: Int {
        get { 0 }
        set {}
    }
}

I don’t think there’s an actual problem here, largely due to the magic of Objective-C.

Consider the small test code at the end of this reply. It declares the requestedMountOptions property without any availability, and I think that’s just fine. Lemme explain…

I put this in an Xcode command-line tool project, set the deployment target to macOS 26.0, and compiled it. I then dumped its imports:

% otool -L MyTool | grep FSKit
% 

It doesn’t import any symbols from FSKit at all. That’s because FSKit is an Objective-C framework and all the stuff used by the class is either found at runtime by name or is a C-style declaration that has no runtime impact [1].

This doesn’t mean that it isn’t connected to the Objective-C runtime. If you run this command:

% otool -o -v MyTool 
… stuff …

you’ll see lots of connection points. However, Objective-C is super dynamic, so that stuff all gets resolved by name when your class is loaded. And the runtime on macOS 26.0 will happily ignore the fact that you’re claiming to implement a requestedMountOptions property that it doesn’t know about.

Now, if this were a Swift framework then the story would be very different. Swift’s protocols are expressed in the dynamic linker and so you really need to get the availability right. But it’s not, and thus this just doesn’t matter.

Or at least that’s my understanding. I don’t have time to do a proper end-to-end test of this. But I suspect that, if you simply leave off the availability annotation, your file system will run on macOS 26.0 and macOS 26.4, with only macOS 26.4 calling your requestedMountOptions getter and setter.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Like FSVolume.MountOptions, which is actually the FSMountOptions C structure.


import Foundation
import FSKit

class MyFileSystem: NSObject, FSVolume.Operations {
    
    override init() {
        fatalError()
    }

    var supportedVolumeCapabilities: FSVolume.SupportedCapabilities { fatalError() }
    
    var volumeStatistics: FSStatFSResult { fatalError() }
    
    func mount(options: FSTaskOptions) async throws {
        fatalError()
    }
    
    … all the other volume operations …
    
    var requestedMountOptions: FSVolume.MountOptions {
        get { fatalError() }
        set { fatalError() }
    }
}

func main() {
    let o = MyFileSystem()
    print(o)
}

main()

Thanks, Quinn and Kevin! I was going to see if I can do a full test of this before I make a release with requestedMountOptions included, but it looks like the availability version has been updated, so I think it's going to be a moot point by the time macOS 26.5 releases. In any case, interesting read as always.

Are read-only filesystems currently supported by FSKit?
 
 
Q