@@ -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 vfs.o vfs_dir.o
+obj-$(CONFIG_$(PHASE_)VFS) += fs_mount.o 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,69 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Mount device driver for the VFS layer
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ */
+
+#include <dm.h>
+#include <malloc.h>
+#include <vfs.h>
+#include "vfs_internal.h"
+#include <dm/device-internal.h>
+#include <dm/lists.h>
+
+int fs_mount_init(struct udevice *vfs, struct udevice *dir,
+ struct udevice *fsdev)
+{
+ struct vfs_priv *priv = dev_get_priv(vfs);
+ struct vfsmount *mnt;
+ char dev_name[30];
+ struct udevice *dev;
+ char *str;
+ int ret;
+
+ snprintf(dev_name, sizeof(dev_name), "mount.%d", priv->mount_count);
+ str = strdup(dev_name);
+ if (!str)
+ return log_msg_ret("fms", -ENOMEM);
+
+ ret = device_bind_driver(vfs, "mount", str, &dev);
+ if (ret) {
+ free(str);
+ return log_msg_ret("fmb", ret);
+ }
+ device_set_name_alloced(dev);
+
+ ret = device_probe(dev);
+ if (ret) {
+ device_unbind(dev);
+ return log_msg_ret("fmp", ret);
+ }
+
+ mnt = dev_get_uclass_priv(dev);
+ mnt->dir = dir;
+ mnt->target = fsdev;
+ priv->mount_count++;
+
+ return 0;
+}
+
+int fs_mount_uninit(struct udevice *mnt_dev)
+{
+ int ret;
+
+ ret = device_remove(mnt_dev, DM_REMOVE_NORMAL);
+ if (ret)
+ return log_msg_ret("fdr", ret);
+
+ ret = device_unbind(mnt_dev);
+ if (ret)
+ return log_msg_ret("fdb", ret);
+
+ return 0;
+}
+
+U_BOOT_DRIVER(mount) = {
+ .name = "mount",
+ .id = UCLASS_MOUNT,
+};
@@ -14,7 +14,10 @@
#include <dir.h>
#include <dm.h>
#include <event.h>
+#include <file.h>
#include <fs.h>
+#include <fs_common.h>
+#include <malloc.h>
#include <vfs.h>
#include "vfs_internal.h"
#include <dm/device-internal.h>
@@ -22,6 +25,150 @@
#include <dm/root.h>
#include <dm/uclass-internal.h>
+#define vfs_foreach_mount(mnt, pos) \
+ for (uclass_first_device(UCLASS_MOUNT, &(pos)); \
+ (pos) && ((mnt) = dev_get_uclass_priv(pos)); \
+ uclass_next_device(&(pos)))
+
+/**
+ * find_mount() - Check whether a directory is a mount point
+ *
+ * @dir: UCLASS_DIR device to check
+ * @mntp: Returns the UCLASS_MOUNT device if found
+ * Return: 0 if found, -ENOENT if not
+ */
+static int find_mount(struct udevice *dir, struct udevice **mntp)
+{
+ struct vfsmount *mnt;
+ struct udevice *dev;
+
+ vfs_foreach_mount(mnt, dev) {
+ if (mnt->dir == dir) {
+ *mntp = dev;
+ return 0;
+ }
+ }
+
+ return -ENOENT;
+}
+
+/**
+ * find_mount_by_target() - Find the mount for a given FS device
+ *
+ * @fsdev: UCLASS_FS device to search for
+ * @mntp: Returns the UCLASS_MOUNT device if found
+ * Return: 0 if found, -ENOENT if not
+ */
+static int find_mount_by_target(struct udevice *fsdev, struct udevice **mntp)
+{
+ struct vfsmount *mnt;
+ struct udevice *dev;
+
+ vfs_foreach_mount(mnt, dev) {
+ if (mnt->target == fsdev) {
+ *mntp = dev;
+ return 0;
+ }
+ }
+
+ return -ENOENT;
+}
+
+/**
+ * walk_path() - Walk a path following mount points
+ *
+ * For each path component, looks up the directory in the current
+ * filesystem and checks if it is a mount point. Stops when a component
+ * cannot be looked up or is not a mount point.
+ *
+ * @path: Path to walk (without leading '/')
+ * @start: Starting FS device
+ * @mntp: Returns the deepest mount found, or NULL if none
+ * @remainp: Returns pointer to the remaining unresolved path
+ * Return: The FS after the deepest mount crossed (same as @start if none)
+ */
+static struct udevice *walk_path(const char *path, struct udevice *start,
+ struct udevice **mntp, const char **remainp)
+{
+ struct udevice *best = NULL, *cur_fs = start;
+ const char *best_remain = path;
+ const char *p = path;
+
+ while (*p) {
+ char component[FS_DIRENT_NAME_LEN];
+ struct udevice *comp_dir, *mnt_dev;
+ struct vfsmount *mnt;
+ const char *slash;
+ int len;
+
+ slash = strchr(p, '/');
+ len = slash ? slash - p : strlen(p);
+ if (len >= sizeof(component))
+ break;
+
+ memcpy(component, p, len);
+ component[len] = '\0';
+
+ if (fs_lookup_dir(cur_fs, component, &comp_dir))
+ break;
+
+ if (find_mount(comp_dir, &mnt_dev))
+ break;
+
+ /* Found a mount - record it and cross into the FS */
+ best = mnt_dev;
+ p += len;
+ if (*p == '/')
+ p++;
+ best_remain = p;
+
+ mnt = dev_get_uclass_priv(mnt_dev);
+ cur_fs = mnt->target;
+ }
+
+ *mntp = best;
+ *remainp = best_remain;
+
+ return cur_fs;
+}
+
+/**
+ * vfs_mount_path() - Build the full path for a mount device
+ *
+ * Walks up the device tree to reconstruct the absolute path, using
+ * dir_uc_priv->path from each mount's directory to get the component name.
+ *
+ * @mnt_dev: UCLASS_MOUNT device
+ * @buf: Buffer to write path into
+ * @size: Size of buffer
+ * Return: 0 if OK, -ve on error
+ */
+static int vfs_mount_path(struct udevice *mnt_dev, char *buf, int size)
+{
+ struct vfsmount *mnt = dev_get_uclass_priv(mnt_dev);
+ struct dir_uc_priv *uc_priv = dev_get_uclass_priv(mnt->dir);
+ struct udevice *parent_fs = dev_get_parent(mnt->dir);
+
+ if (parent_fs == vfs_root()) {
+ snprintf(buf, size, "/%s", uc_priv->path);
+ } else {
+ char parent_path[FILE_MAX_PATH_LEN];
+ struct udevice *pdev;
+ int ret;
+
+ ret = find_mount_by_target(parent_fs, &pdev);
+ if (ret)
+ return ret;
+
+ ret = vfs_mount_path(pdev, parent_path, sizeof(parent_path));
+ if (ret)
+ return ret;
+ snprintf(buf, size, "%s/%s", parent_path, uc_priv->path);
+ }
+
+ return 0;
+}
+
/* VFS root filesystem - provides an empty root directory */
static int vfs_rootfs_mount(struct udevice *dev)
@@ -71,6 +218,112 @@ static int vfs_rootfs_lookup_dir(struct udevice *dev, const char *path,
/* Exported functions */
+int vfs_find_mount(struct udevice *vfs, const char *path, struct udevice **mntp,
+ const char **subpathp)
+{
+ struct udevice *best;
+ const char *p;
+
+ p = path;
+ if (*p == '/')
+ p++;
+
+ walk_path(p, vfs, &best, subpathp);
+
+ if (!best) {
+ if (!*p) {
+ *mntp = NULL;
+ return 0;
+ }
+ return log_msg_ret("vfn", -ENOENT);
+ }
+
+ *mntp = best;
+
+ return 0;
+}
+
+int vfs_resolve(struct udevice *vfs, const char *path,
+ struct udevice **dirp)
+{
+ struct udevice *cur_fs, *best;
+ const char *remain;
+
+ if (!path || *path != '/')
+ return log_msg_ret("vrp", -EINVAL);
+
+ cur_fs = walk_path(path + 1, vfs, &best, &remain);
+
+ /* Remaining path must be at most one component (the target dir) */
+ if (strchr(remain, '/'))
+ return log_msg_ret("vrm", -ENOENT);
+
+ return fs_lookup_dir(cur_fs, remain, dirp);
+}
+
+int vfs_mount(struct udevice *vfs, struct udevice *dir, struct udevice *fsdev)
+{
+ int ret;
+
+ ret = fs_mount(fsdev);
+ if (ret && ret != -EISCONN)
+ return log_msg_ret("vmm", ret);
+
+ ret = fs_mount_init(vfs, dir, fsdev);
+ if (ret) {
+ fs_unmount(fsdev);
+ return log_msg_ret("vmc", ret);
+ }
+
+ return 0;
+}
+
+int vfs_umount(struct udevice *mnt_dev)
+{
+ struct vfsmount *mnt = dev_get_uclass_priv(mnt_dev);
+ int ret;
+
+ ret = fs_unmount(mnt->target);
+ if (ret && ret != -ENOTCONN)
+ return log_msg_ret("vuu", ret);
+
+ ret = fs_mount_uninit(mnt_dev);
+ if (ret)
+ return log_msg_ret("vud", ret);
+
+ return 0;
+}
+
+int vfs_umount_path(struct udevice *vfs, const char *path)
+{
+ struct udevice *mnt_dev;
+ const char *subpath;
+ int ret;
+
+ ret = vfs_find_mount(vfs, path, &mnt_dev, &subpath);
+ if (ret)
+ return log_msg_ret("vuf", ret);
+
+ /* Make sure the entire path was consumed (exact match) */
+ if (!mnt_dev || *subpath)
+ return log_msg_ret("vup", -ENOENT);
+
+ return vfs_umount(mnt_dev);
+}
+
+void vfs_print_mounts(void)
+{
+ struct vfsmount *mnt;
+ struct udevice *dev;
+
+ vfs_foreach_mount(mnt, dev) {
+ char path[FILE_MAX_PATH_LEN];
+
+ if (!vfs_mount_path(dev, path, sizeof(path)))
+ printf("%-20s %s\n", path, mnt->target->name);
+ }
+}
+
struct udevice *vfs_root(void)
{
struct udevice *dev;
@@ -19,4 +19,26 @@ struct vfs_priv {
int mount_count;
};
+/**
+ * fs_mount_init() - Create a mount device
+ *
+ * Binds and probes a UCLASS_MOUNT device as a child of @vfs, linking
+ * @dir to @fsdev.
+ *
+ * @vfs: VFS root FS device
+ * @dir: UCLASS_DIR device that is the mount point
+ * @fsdev: UCLASS_FS device to mount
+ * Return: 0 if OK, -ve on error
+ */
+int fs_mount_init(struct udevice *vfs, struct udevice *dir,
+ struct udevice *fsdev);
+
+/**
+ * fs_mount_uninit() - Remove and unbind a mount device
+ *
+ * @mnt_dev: UCLASS_MOUNT device to destroy
+ * Return: 0 if OK, -ve on error
+ */
+int fs_mount_uninit(struct udevice *mnt_dev);
+
#endif
@@ -46,4 +46,75 @@ int vfs_init(void);
*/
struct udevice *vfs_root(void);
+/**
+ * vfs_resolve() - Resolve a path to a directory
+ *
+ * Walks the path, following mount points along the way. For each
+ * component, looks up the directory in the current filesystem. If the
+ * directory does not exist (e.g. in the VFS rootfs), it is created.
+ *
+ * For "/host", looks up (or creates) "host" in the VFS rootfs.
+ * For "/mnt/data", follows the mount at /mnt, then looks up "data"
+ * in the mounted filesystem.
+ *
+ * @vfs: VFS root FS device
+ * @path: Absolute path (must start with '/')
+ * @dirp: Returns the UCLASS_DIR device for the final component
+ * Return: 0 if OK, -ve on error
+ */
+int vfs_resolve(struct udevice *vfs, const char *path,
+ struct udevice **dirp);
+
+/**
+ * vfs_mount() - Mount a filesystem at a directory
+ *
+ * Creates a UCLASS_MOUNT device linking @dir to @fsdev.
+ *
+ * @vfs: VFS root FS device
+ * @dir: UCLASS_DIR device for the mount point
+ * @fsdev: UCLASS_FS device to mount
+ * Return: 0 if OK, -ve on error
+ */
+int vfs_mount(struct udevice *vfs, struct udevice *dir, struct udevice *fsdev);
+
+/**
+ * vfs_umount() - Unmount a filesystem
+ *
+ * @mnt_dev: UCLASS_MOUNT device to unmount
+ * Return: 0 if OK, -ve on error
+ */
+int vfs_umount(struct udevice *mnt_dev);
+
+/**
+ * vfs_umount_path() - Unmount the filesystem at a path
+ *
+ * @vfs: VFS root FS device
+ * @path: Mount point to remove
+ * Return: 0 if OK, -ENOENT if not mounted, other -ve on error
+ */
+int vfs_umount_path(struct udevice *vfs, const char *path);
+
+/**
+ * vfs_find_mount() - Find the mount covering a path
+ *
+ * Walks the mount tree from the VFS root, following mount points for
+ * each path component. Returns the deepest mount and the remaining
+ * subpath.
+ *
+ * @vfs: VFS root FS device
+ * @path: Absolute path to resolve
+ * @mntp: Returns the UCLASS_MOUNT device
+ * @subpathp: Returns pointer into @path for the remaining path within the
+ * mounted filesystem
+ * Return: 0 if OK (with @mntp set to NULL if path is the VFS root),
+ * -ENOENT if no mount covers this path
+ */
+int vfs_find_mount(struct udevice *vfs, const char *path,
+ struct udevice **mntp, const char **subpathp);
+
+/**
+ * vfs_print_mounts() - Print all current mounts
+ */
+void vfs_print_mounts(void);
+
#endif
@@ -5,6 +5,7 @@
* Copyright 2025 Simon Glass <sjg@chromium.org>
*/
+#include <console.h>
#include <dir.h>
#include <dm.h>
#include <file.h>
@@ -128,10 +129,122 @@ static int dm_test_vfs_init(struct unit_test_state *uts)
ut_asserteq(-ENOENT, dir_read(dir, strm, &dent));
ut_assertok(dir_close(dir, strm));
+ /* vfs_resolve("/") should return the root dir */
+ ut_assertok(vfs_resolve(vfs, "/", &dir));
+ ut_assertnonnull(dir);
+
+ /* vfs_resolve with bad paths should fail */
+ ut_asserteq(-EINVAL, vfs_resolve(vfs, NULL, &dir));
+ ut_asserteq(-EINVAL, vfs_resolve(vfs, "no_slash", &dir));
+
/* rootfs cannot be unmounted */
ut_asserteq(-EBUSY, fs_unmount(vfs));
return 0;
}
DM_TEST(dm_test_vfs_init, UTF_SCAN_FDT);
+
+/* Test that the root directory lists mount points */
+static int dm_test_vfs_dir(struct unit_test_state *uts)
+{
+ struct udevice *vfs, *fsdev, *dir, *root_dir;
+ struct fs_dir_stream *strm;
+ struct fs_dirent dent;
+
+ ut_assertok(vfs_init());
+ vfs = vfs_root();
+ ut_assertnonnull(vfs);
+
+ /* Root dir should be empty before any mounts */
+ ut_assertok(fs_lookup_dir(vfs, "", &root_dir));
+ ut_assertok(dir_open(root_dir, &strm));
+ ut_asserteq(-ENOENT, dir_read(root_dir, strm, &dent));
+ ut_assertok(dir_close(root_dir, strm));
+
+ /* Mount the sandbox FS at /host */
+ ut_assertok(uclass_get_device_by_name(UCLASS_FS, "hostfs", &fsdev));
+ ut_assertok(vfs_resolve(vfs, "/host", &dir));
+ ut_assertok(vfs_mount(vfs, dir, fsdev));
+
+ /* Root dir should now list "host" */
+ ut_assertok(fs_lookup_dir(vfs, "", &root_dir));
+ ut_assertok(dir_open(root_dir, &strm));
+ ut_assertok(dir_read(root_dir, strm, &dent));
+ ut_asserteq_str("host", dent.name);
+ ut_asserteq(FS_DT_DIR, dent.type);
+ ut_asserteq(-ENOENT, dir_read(root_dir, strm, &dent));
+ ut_assertok(dir_close(root_dir, strm));
+
+ ut_assertok(vfs_umount_path(vfs, "/host"));
+
+ return 0;
+}
+DM_TEST(dm_test_vfs_dir, UTF_SCAN_FDT);
+
+/* Test basic VFS mount, find_mount, ls and umount */
+static int dm_test_vfs_mount(struct unit_test_state *uts)
+{
+ struct udevice *vfs, *fsdev, *dir, *mnt;
+ const char *subpath;
+
+ ut_assertok(vfs_init());
+ vfs = vfs_root();
+ ut_assertnonnull(vfs);
+
+ /* Find the sandbox FS (not the vfs_rootfs) */
+ ut_assertok(uclass_get_device_by_name(UCLASS_FS, "hostfs", &fsdev));
+
+ /* Resolve /host to a mount-point DIR */
+ ut_assertok(vfs_resolve(vfs, "/host", &dir));
+
+ /* Mount the sandbox FS at /host */
+ ut_assertok(vfs_mount(vfs, dir, fsdev));
+
+ /* Mounting same FS at another path is OK (-EISCONN ignored) */
+ ut_assertok(vfs_resolve(vfs, "/other", &dir));
+ ut_assertok(vfs_mount(vfs, dir, fsdev));
+ ut_assertok(vfs_umount_path(vfs, "/other"));
+
+ /* vfs_print_mounts() should show the /host mount */
+ console_record_reset_enable();
+ vfs_print_mounts();
+ ut_assert_nextlinen("/host");
+ ut_assert_console_end();
+
+ /* find_mount should resolve /host exactly */
+ ut_assertok(vfs_find_mount(vfs, "/host", &mnt, &subpath));
+ ut_asserteq_str("", subpath);
+
+ /* find_mount should strip mount prefix from subpath */
+ ut_assertok(vfs_find_mount(vfs, "/host/some/path", &mnt, &subpath));
+ ut_asserteq_str("some/path", subpath);
+
+ /* find_mount should handle trailing component */
+ ut_assertok(vfs_find_mount(vfs, "/host/file.txt", &mnt, &subpath));
+ ut_asserteq_str("file.txt", subpath);
+
+ /* find_mount should fail for unmounted path */
+ ut_asserteq(-ENOENT, vfs_find_mount(vfs, "/nowhere", &mnt, &subpath));
+
+ /* find_mount with partial prefix should not match */
+ ut_asserteq(-ENOENT, vfs_find_mount(vfs, "/hostal", &mnt, &subpath));
+
+ /* vfs_resolve with intermediate non-mount should fail */
+ ut_asserteq(-ENOENT, vfs_resolve(vfs, "/bogus/sub", &dir));
+
+ /* Unmount */
+ ut_assertok(vfs_umount_path(vfs, "/host"));
+
+ /* Should not be mounted any more */
+ ut_asserteq(-ENOENT, vfs_find_mount(vfs, "/host", &mnt, &subpath));
+
+ /* Double umount should fail */
+ ut_asserteq(-ENOENT, vfs_umount_path(vfs, "/host"));
+
+ /* Umount of never-mounted path should fail */
+ ut_asserteq(-ENOENT, vfs_umount_path(vfs, "/bogus"));
+
+ return 0;
+}
+DM_TEST(dm_test_vfs_mount, UTF_SCAN_FDT);
#endif