Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mSL/FHS

C/C++ CI License: MIT Platform

macOS Subsystem for Linux / Filesystem Hierarchy Standard — a native implementation of the Linux filesystem layout on macOS.

One module of mSL/XNU, a modular macOS Subsystem for Linux.

The larger project

mSL/XNU — macOS Subsystem for Linux / X is Now UNIX — aims at native, seamless execution of Linux ELF binaries on macOS: not in a container and not in a virtual machine, but as ordinary processes on the running system.

Reaching that needs several independent pieces, which is why the project is modular rather than one monolith. Each is useful on its own, and each can be installed, replaced or omitted:

Piece What it does Where
Filesystem Hierarchy Standard The Linux filesystem layout, natively this repository
Syscall translation Linux system calls onto Darwin's, over Hypervisor.framework mSL/NABI
procfs /proc, as a real filesystem mSL/ProcFS
sysfs /sys, likewise mSL/SysFS
devfs /dev — already part of macOS XNU

This repository is the FHS piece, and it is largely done. The rest of this document describes it.

What is mSL/FHS?

macOS is a certified UNIX, and nearly everything a Linux program expects from the filesystem is already present on the machine — it is simply in a different place, under a different name, or hidden from view. Home directories live in /Users rather than /home. Mounted volumes appear in /Volumes rather than /media. /proc and /sys do not exist at all.

mSL/FHS closes that gap at the namespace level. It is not a container, an emulator, or a virtual machine: there is no second kernel and no translation layer. The processes are macOS processes, the filesystems are macOS filesystems, and the kernel is XNU. Only the shape of the namespace changes, so that a program — or a person — expecting the Filesystem Hierarchy Standard finds what it is looking for.

That modesty is the point. A Linux binary needs its paths to resolve before any of the harder machinery matters, and paths are the one part of the problem that needs no kernel code at all: macOS already has the content, under other names. This module supplies the names, and nothing more.

Within it, the work splits again:

  • The layout (this repository) — the static shape of the tree. Home directories, mount points, removable media, the boot artifacts. Built out of symlinks maintained by a small daemon, with no kernel code.
  • The pseudo-filesystems (separate modules) — the synthetic, dynamic parts of a Linux tree, which must be real filesystems because their contents are generated on demand: /proc, /sys, and macOS's own /dev.

mSL/FHS detects and reports the status of the pseudo-filesystems, but never mounts, unmounts, or configures them — each keeps its own installer and its own toggles. This avoids two applications fighting over the same mount point and reporting state that contradicts reality.

Tested on:

- macOS 26.5.2 (Tahoe), Darwin 25.5.0, Apple Silicon (arm64) — primary target

Design

Two tiers

The single most important constraint on macOS is that the root directory is not writable. The system volume is sealed (APFS Signed System Volume) and mounted read-only; System Integrity Protection prevents modification even as root. A directory cannot simply be created at /.

The only supported mechanism for adding entries to / is /etc/synthetic.conf, which is parsed exactly once per boot, early, by apfs_boot_util. Nothing can make it take effect again without a restart. That single fact splits the layer in two:

Tier What it is When it changes In the toggle?
Skeleton The root-level entries themselves (/home, /mnt, /media, …), created as symlinks by synthetic.conf Install time only; requires a reboot No
Contents What lives inside them — the per-user and per-volume symlinks, maintained by fhsxd Continuously, at runtime Yes

The skeleton is inert. A /media symlink pointing at an empty directory is indistinguishable, to any program, from a system that never had /media — so leaving it in place while the layer is switched off costs nothing. All the observable behaviour lives in the contents tier, which is fully dynamic.

The practical consequence: installing mSL/FHS requires one reboot, and so does adding a new root-level directory later. Everything else — enabling a component, disabling it, uninstalling the layer's effects — happens live. This is a real limitation of the platform rather than of the implementation, and the installer states it plainly.

Symlink farms, not mounts

Most components are a directory on the writable Data volume, reached through a symlink at /, whose contents are symlinks maintained by the daemon:

/media -> /System/Volumes/Data/media
          └── sunneva/
              └── USB_STICK -> /Volumes/USB STICK

This was chosen over the more obvious alternatives:

  • autofs maps can be reloaded live (automount -c) and are the right tool for /home, where macOS already uses them — but they mount filesystems, and macOS has no loopback/bind filesystem to redirect one existing path to another. They also enumerate poorly, so ls /media/$USER would not reliably list what is mounted.
  • A kernel extension could hook lookup and synthesise these paths, but on Apple Silicon that costs Reduced Security, a user-approved load, an auxiliary kernel collection rebuild, and a reboot — an enormous price for a symlink, on a mechanism Apple continues to narrow. The pseudo-filesystems justify that cost because they genuinely must synthesise content in the kernel. This layer does not.

/root and /run are simpler still: their symlink points straight at the macOS directory that already holds the content, so there is nothing beneath them to maintain at all.

Symlinks are boring, inspectable with ls -l, removable without privilege escalation, and leave nothing behind when deleted.

Components

Each is independently toggleable. See docs/LAYOUT.md for the full per-directory semantics.

Component Provides Populated by Notes
home /home/<user>/Users/<user> One symlink per local user Also masks the auto_home automounter map
mnt /mnt Nothing — the administrator mounts into it Reported, never mounted by mSL
media /media/<user>/<label>/Volumes/<label> DiskArbitration events, live Removable media only; filtered and user-attributed
boot /boot Symlinks to the kernel, bootloader and kernel collections Names them; copies and modifies nothing
root /root/var/root Names existing content; creates nothing
run /run/var/run Names existing content; creates nothing
srv /srv Nothing — stays empty Empty on most Linux systems too

/root and /run are the cheapest components here and the most honest: macOS already keeps the superuser's home at /var/root and runtime state at /var/run, which is exactly what Linux calls /root and /run. The entry gives that content the name Linux uses and creates no second copy — so there is nothing to synchronise and nothing that can drift. ls /run lists the pid files and sockets the system is actually using.

/boot gathers what this machine actually boots — the per-SoC kernel, the bootloader, the kernel collections — under the names Linux uses, by symlink. Nothing is copied or modified. It is also the one component built with the larger project in view: it is a directory this layer owns on the writable Data volume rather than a symlink onto a read-only system path, so a real Linux kernel image can sit beside the Darwin ones later without rearranging anything.

/mnt staying empty is not an unimplemented feature. The FHS defines it as scratch space for temporary, manual mounts (mkdir /mnt/disk1 && mount /dev/… /mnt/disk1); nothing populates it automatically on Linux, and mapping it onto /Volumes would produce a layout no Linux system actually has. The component does report what you have mounted there — read-only; it never mounts or unmounts anything.

/home needs the automounter masked because, while the auto_home map is active, autofs owns the directory and nothing can be created in it. That has a consequence worth stating plainly: the root-level /home is not a permanent part of macOS — autofs creates it at boot from that very map. Masking the map therefore removes /home itself at the next boot, so the component declares its own /home in synthetic.conf alongside /mnt and /media.

/media is the auto-populated one, and it is the component that needs real work. macOS puts everything in /Volumes — internal volumes, network shares, mounted disk images, and removable devices, flat and without user attribution — whereas Linux's /media/<user>/<label> contains only removable media mounted by that user's session. Reproducing it faithfully means filtering by device class and matching udisks2's naming rules, which is what fhsxd does.

Components at runtime

fhsxd is a root LaunchDaemon (KeepAlive), mirroring the procfsd design:

  • Reconciles every enabled component at startup — the boot-time restore
  • Subscribes to DiskArbitration and keeps /media in step as volumes appear and disappear
  • Follows the console user, so a login or fast user switch re-attributes /media
  • Polls slowly (60s) for account changes, which nothing notifies on
  • Will serve state queries and toggle requests to the GUI

It holds no state of its own. Every wakeup re-reads the system and reconciles, so a missed event is corrected by the next one rather than leaving the daemon permanently out of step, and KeepAlive restarting it is always safe. It also calls the same sync functions fhsctl does, so the two can never disagree about what should exist.

Mounting one volume produces several DiskArbitration callbacks — appeared, then description changes as the mount completes. Each schedules a single short-delay timer rather than reconciling immediately, so a burst collapses into one pass and the volume is never observed mid-mount before its path is set.

Privileged actions go through the standard macOS administrator-authorization prompt, as procfs's companion app does. With a per-component matrix that would mean a prompt per checkbox, so the preference pane batches: pending changes are collected and applied in one authorized step.

Routing mutations through the daemon instead — it is already root, so it could apply them without a prompt — was considered and rejected. It would mean a root process accepting reconfiguration commands over a socket, and any peer policy permissive enough to be convenient would also let anything running as the user silently remask /etc/auto_master. The prompt is not only an access check: it is what makes a change to the system visible and attributable to the person making it. fhsxd's command surface stays closed.

Interface

Menu bar app — live status for each component and the detected pseudo-filesystems, one-click toggles, and quick access to preferences.

Preference pane (/Library/PreferencePanes/FHS.prefPane) — per-component enable/disable, start-at-boot, and update settings.

Both read state live rather than caching it, so they never disagree with the filesystem.

Requirements

  • macOS 15 or later (Apple Silicon or Intel)
  • Administrator access for installation
  • One reboot before the root-level directories appear

No kernel extension, no Reduced Security, and no SIP changes are required for the layout layer. (The optional pseudo-filesystems have their own requirements.)

Installing

From a release disk image, open the .pkg and follow the installer. From source:

make                # build everything into out/
sudo make install   # install and start the daemon

Either way, nothing is switched on by the install. Enabling a component edits system configuration — every component adds an entry to /etc/synthetic.conf, and /home additionally masks a line in /etc/auto_master — and an installer making changes that affect login, on a machine whose setup it has not inspected, would be taking a decision that belongs to the person running it.

Turn components on in System Settings → mSL/FHS, or from the menu bar, or:

fhsctl status       # what is on, and what state it is in
fhsctl home check   # is /home safe to enable on this machine?
sudo fhsctl home enable

All three appear after a restart: macOS creates root-level entries only at startup. The symlinks beneath them are built immediately, so everything is in place when the directories appear.

To show or hide the root-level directories in the Finder:

fhsctl vis                    # every node, and whether it can be changed
sudo fhsctl vis show opt
sudo fhsctl vis hide opt

To build a distributable installer:

make dmg        # out/mSL-XNU-<version>.dmg, containing the .pkg and uninstaller
make distcheck  # clean build, then verify the payload carries every component

Uninstalling

Uninstall mSL.command from the disk image, or sudo make uninstall from source. Both switch every component off before removing anything — disabling is what restores /etc/auto_master and drops the synthetic.conf entries, so removing the tools first would strand the system with a masked automounter line and no supported way to restore it. A pristine copy of /etc/auto_master is kept at /var/db/fhs.auto_master.orig regardless.

Status

Working, and verified on macOS 26.5.2 (Tahoe), Darwin 25.5.0, Apple Silicon.

Component Status
/home Working — survives reboot via its own synthetic.conf entry
/mnt Working — reports filesystems mounted under it
/media Working — tracks volumes live through DiskArbitration
/boot Working — names this machine's kernel and bootloader
/root /run Working — name the existing /var/root and /var/run
/srv Working — empty, as on Linux
Finder visibility Working — for the nodes macOS permits; see below
fhsctl Working — CLI control for the layer
fhsxd Working — boot restore, live volume tracking, console-user changes
Menu bar app Working — per-node dropdowns
Preference pane Working — batched Apply
Installer Working — built by make, with an uninstaller app
/proc detection Working (procfs exists and is mature)
/sys detection Working — reports "not installed" until sysfs exists

Finder visibility

macOS hides most root-level directories from the Finder with the UF_HIDDEN file flag. mSL/FHS can clear it — but only where the platform permits, and which those are is not apparent from the path. Measured, not assumed:

Nodes Result
/opt /cores /Volumes Changeable — firmlinked to the writable Data volume
/bin /etc /sbin /tmp /usr /var SF_RESTRICTED — refused by SIP
/home and other root symlinks Read-only — the entry itself is on the sealed root
/private Refused, for a reason nothing exposes
/dev Blocked three ways — see below

An earlier version of this file claimed root-directory unhiding was uniformly impossible under SIP. That was wrong: three nodes are genuinely changeable, and clearing /opt's flag makes it fully visible in the Finder, not merely dimmed.

The GUI shows a locked node's toggle disabled with the reason, rather than offering a control that cannot work. Every change is verified by re-reading the flag afterwards, because one filesystem accepts the call and ignores it.

/dev cannot be shown in the Finder by any supported means. All three routes were tried:

  • devfs accepts chflags and silently ignores it, so UF_HIDDEN cannot be cleared
  • devfs does not implement MNT_UPDATE, so mount -u cannot clear nobrowse
  • SIP's rootless protection refuses a second devfs stacked over /dev, EPERM even as root

That leaves a kernel extension as the only path, for one cosmetic directory — so /dev visibility is deferred alongside the sysfs work rather than attempted here.

Not planned, and why

Unhiding the SIP-protected and sealed directories. /bin, /etc, /usr, /var and friends live on the Signed System Volume, whose seal is a cryptographic hash tree verified at boot. The hidden flag on those entries is part of the sealed content: there is no writable inode to change, from userspace or from a kernel extension, and altering it would break the seal and prevent the machine from booting. Only a VFS-interception layer that synthesises a rewritten view of / could do it — a different and much larger undertaking, and one that belongs with the pseudo-filesystem work rather than here.

Any program, shell, or script already sees these paths normally; this affects one application's presentation.

License

MIT — see LICENSE.

About

macOS Subsystem for Linux / Filesystem Hierarchy Standard (mSL/FHS)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages