@@ -32,6 +32,7 @@ config FILE
config VFS
bool "Virtual filesystem support"
depends on FS && DIR
+ select EVENT
help
Provides a virtual filesystem layer with a mount table and
unified path namespace. Includes a root filesystem at "/".
@@ -8,7 +8,7 @@ obj-$(CONFIG_$(PHASE_)FS_LEGACY) += fs_legacy.o fs_internal.o
obj-$(CONFIG_$(PHASE_)FS) += fs-uclass.o
obj-$(CONFIG_$(PHASE_)DIR) += dir-uclass.o
obj-$(CONFIG_$(PHASE_)FILE) += file-uclass.o
-obj-$(CONFIG_$(PHASE_)VFS) += mount-uclass.o
+obj-$(CONFIG_$(PHASE_)VFS) += mount-uclass.o vfs.o vfs_dir.o
ifdef CONFIG_XPL_BUILD
obj-$(CONFIG_SPL_FS_FAT) += fat/
new file mode 100644
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Virtual Filesystem layer
+ *
+ * Manages a mount tree using UCLASS_MOUNT devices as children of
+ * UCLASS_DIR devices, providing unified path resolution across mounted
+ * filesystems. Inspired by the Linux VFS.
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY UCLASS_MOUNT
+
+#include <dir.h>
+#include <dm.h>
+#include <event.h>
+#include <fs.h>
+#include <vfs.h>
+#include "vfs_internal.h"
+#include <dm/device-internal.h>
+#include <dm/lists.h>
+#include <dm/root.h>
+#include <dm/uclass-internal.h>
+
+/* VFS root filesystem - provides an empty root directory */
+
+static int vfs_rootfs_mount(struct udevice *dev)
+{
+ struct fs_priv *uc_priv = dev_get_uclass_priv(dev);
+
+ if (uc_priv->mounted)
+ return log_msg_ret("rfm", -EISCONN);
+
+ uc_priv->mounted = true;
+
+ return 0;
+}
+
+static int vfs_rootfs_unmount(struct udevice *dev)
+{
+ return log_msg_ret("rfu", -EBUSY);
+}
+
+static int vfs_rootfs_lookup_dir(struct udevice *dev, const char *path,
+ struct udevice **dirp)
+{
+ struct dir_uc_priv *uc_priv;
+ struct udevice *child, *dir;
+ int ret;
+
+ /* Check for an existing dir with this path */
+ device_foreach_child(child, dev) {
+ if (device_get_uclass_id(child) == UCLASS_DIR) {
+ uc_priv = dev_get_uclass_priv(child);
+ if (uc_priv->path && !strcmp(uc_priv->path, path)) {
+ *dirp = child;
+ return 0;
+ }
+ }
+ }
+
+ /* Create a new dir */
+ ret = dir_add_probe(dev, DM_DRIVER_GET(vfs_rootfs_dir), path, &dir);
+ if (ret)
+ return log_msg_ret("rfD", ret);
+
+ *dirp = dir;
+
+ return 0;
+}
+
+/* Exported functions */
+
+struct udevice *vfs_root(void)
+{
+ struct udevice *dev;
+
+ if (uclass_find_device_by_name(UCLASS_FS, "vfs_rootfs", &dev))
+ return NULL;
+ if (!device_active(dev))
+ return NULL;
+
+ return dev;
+}
+
+int vfs_init(void)
+{
+ struct udevice *dev;
+ int ret;
+
+ /* Already initialised? */
+ dev = vfs_root();
+ if (dev)
+ return 0;
+
+ ret = device_bind_driver(dm_root(), "vfs_rootfs", "vfs_rootfs", &dev);
+ if (ret)
+ return log_msg_ret("vib", ret);
+
+ ret = device_probe(dev);
+ if (ret)
+ return log_msg_ret("vip", ret);
+
+ ret = fs_mount(dev);
+ if (ret)
+ return log_msg_ret("vim", ret);
+
+ return 0;
+}
+
+static const struct fs_ops vfs_rootfs_ops = {
+ .mount = vfs_rootfs_mount,
+ .unmount = vfs_rootfs_unmount,
+ .lookup_dir = vfs_rootfs_lookup_dir,
+};
+
+U_BOOT_DRIVER(vfs_rootfs) = {
+ .name = "vfs_rootfs",
+ .id = UCLASS_FS,
+ .ops = &vfs_rootfs_ops,
+ .priv_auto = sizeof(struct vfs_priv),
+};
+
+EVENT_SPY_SIMPLE(EVT_LAST_STAGE_INIT, vfs_init);
new file mode 100644
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * VFS root directory driver
+ *
+ * Provides the directory driver for the VFS rootfs. The root directory
+ * (path "") lists all mount-point directories as entries. Mount-point
+ * directories themselves have no entries since access crosses into the
+ * mounted filesystem.
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ */
+
+#include <dir.h>
+#include <dm.h>
+#include <fs.h>
+#include <fs_common.h>
+
+static int vfs_rootfs_dir_open(struct udevice *dev,
+ struct fs_dir_stream *strm)
+{
+ strm->offset = 0;
+
+ return 0;
+}
+
+/**
+ * vfs_rootfs_dir_read() - Read the next directory entry
+ *
+ * For the root directory (path ""), iterates through sibling DIR devices
+ * that have non-empty paths, returning each as a directory entry. For
+ * mount-point directories, returns -ENOENT immediately since their
+ * contents come from the mounted filesystem.
+ */
+static int vfs_rootfs_dir_read(struct udevice *dev,
+ struct fs_dir_stream *strm,
+ struct fs_dirent *dent)
+{
+ struct dir_uc_priv *dir_priv = dev_get_uclass_priv(dev);
+ struct udevice *parent, *child;
+ int idx = 0;
+
+ /* Only the root dir has entries to list */
+ if (*dir_priv->path)
+ return -ENOENT;
+
+ /* Walk children of the FS device, skipping to the current offset */
+ parent = dev_get_parent(dev);
+ device_foreach_child(child, parent) {
+ struct dir_uc_priv *uc_priv;
+
+ if (device_get_uclass_id(child) != UCLASS_DIR)
+ continue;
+
+ uc_priv = dev_get_uclass_priv(child);
+ if (!uc_priv->path || !*uc_priv->path)
+ continue;
+
+ if (idx++ < strm->offset)
+ continue;
+
+ strlcpy(dent->name, uc_priv->path, sizeof(dent->name));
+ dent->type = FS_DT_DIR;
+ dent->size = 0;
+ strm->offset++;
+
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int vfs_rootfs_dir_close(struct udevice *dev,
+ struct fs_dir_stream *strm)
+{
+ return 0;
+}
+
+static struct dir_ops vfs_rootfs_dir_ops = {
+ .open = vfs_rootfs_dir_open,
+ .read = vfs_rootfs_dir_read,
+ .close = vfs_rootfs_dir_close,
+};
+
+U_BOOT_DRIVER(vfs_rootfs_dir) = {
+ .name = "vfs_rootfs_dir",
+ .id = UCLASS_DIR,
+ .ops = &vfs_rootfs_dir_ops,
+};
new file mode 100644
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Internal VFS helpers - not part of the public API
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ */
+
+#ifndef __VFS_INTERNAL_H
+#define __VFS_INTERNAL_H
+
+struct udevice;
+
+/**
+ * struct vfs_priv - VFS root FS device driver-private data
+ *
+ * @mount_count: Counter for unique mount device names
+ */
+struct vfs_priv {
+ int mount_count;
+};
+
+#endif
@@ -28,4 +28,22 @@ struct vfsmount {
struct udevice *target;
};
+/**
+ * vfs_init() - Initialise the VFS
+ *
+ * Creates the VFS root directory device. Normally called automatically
+ * via EVT_LAST_STAGE_INIT during boot. May also be called directly in
+ * tests after a DM tree reset.
+ *
+ * Return: 0 if OK, -ve on error
+ */
+int vfs_init(void);
+
+/**
+ * vfs_root() - Get the VFS root FS device
+ *
+ * Return: VFS root FS device, or NULL if not initialised
+ */
+struct udevice *vfs_root(void);
+
#endif
@@ -9,6 +9,7 @@
#include <dm.h>
#include <file.h>
#include <fs.h>
+#include <vfs.h>
#include <dm/test.h>
#include <test/ut.h>
@@ -104,3 +105,33 @@ static int dm_test_fs_file(struct unit_test_state *uts)
return 0;
}
DM_TEST(dm_test_fs_file, UTF_SCAN_FDT);
+
+#if IS_ENABLED(CONFIG_VFS)
+/* Test VFS init and root directory operations */
+static int dm_test_vfs_init(struct unit_test_state *uts)
+{
+ struct udevice *vfs, *dir;
+ struct fs_dir_stream *strm;
+ struct fs_dirent dent;
+
+ ut_assertok(vfs_init());
+
+ vfs = vfs_root();
+ ut_assertnonnull(vfs);
+
+ /* Look up the root directory */
+ ut_assertok(fs_lookup_dir(vfs, "", &dir));
+ ut_assertnonnull(dir);
+
+ /* open should succeed, read should return -ENOENT (root is empty) */
+ ut_assertok(dir_open(dir, &strm));
+ ut_asserteq(-ENOENT, dir_read(dir, strm, &dent));
+ ut_assertok(dir_close(dir, strm));
+
+ /* rootfs cannot be unmounted */
+ ut_asserteq(-EBUSY, fs_unmount(vfs));
+
+ return 0;
+}
+DM_TEST(dm_test_vfs_init, UTF_SCAN_FDT);
+#endif
@@ -21,6 +21,7 @@ EVT_LAST_STAGE_INIT alloc_write_acpi_tables .*lib/acpi/acpi_table.c:.*
EVT_LAST_STAGE_INIT efi_block_device_create .*lib/efi_driver/efi_block_device.c:.*
EVT_LAST_STAGE_INIT install_smbios_table .*lib/efi_loader/efi_smbios.c:.*
EVT_LAST_STAGE_INIT last_stage_init .*arch/sandbox/cpu/start.c:.*
+EVT_LAST_STAGE_INIT vfs_init .*fs/vfs.c:.*
EVT_MISC_INIT_F sandbox_early_getopt_check .*arch/sandbox/cpu/start.c:.*
EVT_SETTINGS_R sandbox_settings .*board/sandbox/sandbox.c:.*
EVT_TEST h_adder_simple .*test/common/event.c:'''