diff --git a/fs/Makefile b/fs/Makefile
index 704ac6e4866..345a4627241 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -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/
diff --git a/fs/fs_mount.c b/fs/fs_mount.c
new file mode 100644
index 00000000000..11e7302eb18
--- /dev/null
+++ b/fs/fs_mount.c
@@ -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,
+};
diff --git a/fs/vfs.c b/fs/vfs.c
index 439c9760829..c42e60b4dc6 100644
--- a/fs/vfs.c
+++ b/fs/vfs.c
@@ -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;
diff --git a/fs/vfs_internal.h b/fs/vfs_internal.h
index b7e6b37d55d..ac7f756e24f 100644
--- a/fs/vfs_internal.h
+++ b/fs/vfs_internal.h
@@ -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
diff --git a/include/vfs.h b/include/vfs.h
index 1bf1762ab52..d7b6449b088 100644
--- a/include/vfs.h
+++ b/include/vfs.h
@@ -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
diff --git a/test/dm/fs.c b/test/dm/fs.c
index 1d395031dee..f31a11e90cb 100644
--- a/test/dm/fs.c
+++ b/test/dm/fs.c
@@ -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
