diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 6ae44a2d0a3..f79baccc6a7 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -9,3 +9,4 @@
 
 obj-y := ext4fs.o ext4_common.o dev.o
 obj-$(CONFIG_EXT4_WRITE) += ext4_write.o ext4_journal.o
+obj-$(CONFIG_$(PHASE_)VFS) += fs.o
diff --git a/fs/ext4/fs.c b/fs/ext4/fs.c
new file mode 100644
index 00000000000..09cbddee50f
--- /dev/null
+++ b/fs/ext4/fs.c
@@ -0,0 +1,264 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ext4 filesystem driver for the VFS layer
+ *
+ * Wraps the existing ext4 implementation to provide UCLASS_FS and
+ * UCLASS_DIR devices, following the same pattern as the FAT VFS driver.
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY	UCLASS_FS
+
+#include <blk.h>
+#include <dir.h>
+#include <dm.h>
+#include <ext4fs.h>
+#include <file.h>
+#include <fs.h>
+#include <iovec.h>
+#include <malloc.h>
+#include <vfs.h>
+#include <dm/device-internal.h>
+
+/**
+ * struct ext4_dir_priv - Private info for ext4 directory devices
+ *
+ * @strm: Directory stream from ext4fs_opendir(), or NULL
+ */
+struct ext4_dir_priv {
+	struct fs_dir_stream *strm;
+};
+
+static int ext4_vfs_mount(struct udevice *dev)
+{
+	struct fs_priv *uc_priv = dev_get_uclass_priv(dev);
+	struct fs_plat *plat = dev_get_uclass_plat(dev);
+	int ret;
+
+	if (uc_priv->mounted)
+		return log_msg_ret("emm", -EISCONN);
+
+	if (!plat->desc)
+		return log_msg_ret("emd", -ENODEV);
+
+	ret = ext4fs_probe(plat->desc, &plat->part);
+	if (ret)
+		return log_msg_ret("emp", ret);
+
+	uc_priv->mounted = true;
+
+	return 0;
+}
+
+static int ext4_vfs_unmount(struct udevice *dev)
+{
+	struct fs_priv *uc_priv = dev_get_uclass_priv(dev);
+
+	if (!uc_priv->mounted)
+		return log_msg_ret("euu", -ENOTCONN);
+
+	ext4fs_close();
+	uc_priv->mounted = false;
+
+	return 0;
+}
+
+static int ext4_vfs_lookup_dir(struct udevice *dev, const char *path,
+			       struct udevice **dirp)
+{
+	struct udevice *dir;
+	int ret;
+
+	ret = dir_add_probe(dev, DM_DRIVER_GET(ext4_vfs_dir), path, &dir);
+	if (ret)
+		return log_msg_ret("eld", ret);
+
+	*dirp = dir;
+
+	return 0;
+}
+
+#if IS_ENABLED(CONFIG_EXT4_WRITE)
+static int ext4_vfs_unlink(struct udevice *dev, const char *path)
+{
+	return ext4fs_filename_unlink((char *)path);
+}
+
+static int ext4_vfs_ln(struct udevice *dev, const char *path,
+		       const char *target)
+{
+	return ext4fs_create_link(target, path);
+}
+#endif
+
+static const struct fs_ops ext4_vfs_ops = {
+	.mount		= ext4_vfs_mount,
+	.unmount	= ext4_vfs_unmount,
+	.lookup_dir	= ext4_vfs_lookup_dir,
+#if IS_ENABLED(CONFIG_EXT4_WRITE)
+	.unlink		= ext4_vfs_unlink,
+	.ln		= ext4_vfs_ln,
+#endif
+};
+
+U_BOOT_DRIVER(ext4_old_fs) = {
+	.name	= "ext4_old_fs",
+	.id	= UCLASS_FS,
+	.ops	= &ext4_vfs_ops,
+};
+
+/* ext4 directory driver */
+
+static int ext4_dir_open(struct udevice *dev, struct fs_dir_stream *strm)
+{
+	struct ext4_dir_priv *priv = dev_get_priv(dev);
+	struct dir_uc_priv *uc_priv = dev_get_uclass_priv(dev);
+	struct fs_dir_stream *ext4_strm;
+	const char *path;
+	int ret;
+
+	path = *uc_priv->path ? uc_priv->path : "/";
+	ret = ext4fs_opendir(path, &ext4_strm);
+	if (ret)
+		return log_msg_ret("edo", ret);
+
+	priv->strm = ext4_strm;
+
+	return 0;
+}
+
+static int ext4_dir_read(struct udevice *dev, struct fs_dir_stream *strm,
+			 struct fs_dirent *dent)
+{
+	struct ext4_dir_priv *priv = dev_get_priv(dev);
+	struct fs_dirent *ext4_dent;
+	int ret;
+
+	ret = ext4fs_readdir(priv->strm, &ext4_dent);
+	if (ret)
+		return ret;
+
+	*dent = *ext4_dent;
+
+	return 0;
+}
+
+static int ext4_dir_close(struct udevice *dev, struct fs_dir_stream *strm)
+{
+	struct ext4_dir_priv *priv = dev_get_priv(dev);
+
+	ext4fs_closedir(priv->strm);
+	priv->strm = NULL;
+
+	return 0;
+}
+
+/* ext4 file driver */
+
+/**
+ * struct ext4_file_priv - Private info for ext4 file devices
+ *
+ * @path: Full path within the ext4 filesystem
+ */
+struct ext4_file_priv {
+	char path[FILE_MAX_PATH_LEN];
+};
+
+static ssize_t ext4_read_iter(struct udevice *dev, struct iov_iter *iter,
+			      loff_t pos)
+{
+	struct ext4_file_priv *priv = dev_get_priv(dev);
+	loff_t actual;
+	int ret;
+
+	ret = ext4_read_file(priv->path, iter_iov_ptr(iter), pos,
+			     iter_iov_avail(iter), &actual);
+	if (ret)
+		return log_msg_ret("efr", ret);
+	iter_advance(iter, actual);
+
+	return actual;
+}
+
+#if IS_ENABLED(CONFIG_EXT4_WRITE)
+static ssize_t ext4_write_iter(struct udevice *dev, struct iov_iter *iter,
+			       loff_t pos)
+{
+	struct ext4_file_priv *priv = dev_get_priv(dev);
+	loff_t actual;
+	int ret;
+
+	ret = ext4_write_file(priv->path, (void *)iter_iov_ptr(iter), pos,
+			      iter_iov_avail(iter), &actual);
+	if (ret)
+		return log_msg_ret("efw", ret);
+	iter_advance(iter, actual);
+
+	return actual;
+}
+#endif
+
+static struct file_ops ext4_file_ops = {
+	.read_iter	= ext4_read_iter,
+#if IS_ENABLED(CONFIG_EXT4_WRITE)
+	.write_iter	= ext4_write_iter,
+#endif
+};
+
+U_BOOT_DRIVER(ext4_vfs_file) = {
+	.name		= "ext4_vfs_file",
+	.id		= UCLASS_FILE,
+	.ops		= &ext4_file_ops,
+	.priv_auto	= sizeof(struct ext4_file_priv),
+};
+
+static int ext4_dir_open_file(struct udevice *dir, const char *leaf,
+			      enum dir_open_flags_t oflags,
+			      struct udevice **filp)
+{
+	struct dir_uc_priv *uc_priv = dev_get_uclass_priv(dir);
+	struct ext4_file_priv *priv;
+	struct udevice *dev;
+	char path[FILE_MAX_PATH_LEN];
+	loff_t size = 0;
+	int ret;
+
+	if (*uc_priv->path)
+		snprintf(path, sizeof(path), "%s/%s", uc_priv->path, leaf);
+	else
+		snprintf(path, sizeof(path), "/%s", leaf);
+
+	if (oflags == DIR_O_RDONLY) {
+		if (!ext4fs_exists(path))
+			return log_msg_ret("eoe", -ENOENT);
+		ret = ext4fs_size(path, &size);
+		if (ret)
+			return log_msg_ret("eos", ret);
+	}
+
+	ret = file_add_probe(dir, DM_DRIVER_REF(ext4_vfs_file), leaf,
+			     size, oflags, &dev);
+	if (ret)
+		return log_msg_ret("eop", ret);
+
+	priv = dev_get_priv(dev);
+	strlcpy(priv->path, path, sizeof(priv->path));
+	*filp = dev;
+
+	return 0;
+}
+
+static struct dir_ops ext4_dir_ops = {
+	.open		= ext4_dir_open,
+	.read		= ext4_dir_read,
+	.close		= ext4_dir_close,
+	.open_file	= ext4_dir_open_file,
+};
+
+U_BOOT_DRIVER(ext4_vfs_dir) = {
+	.name		= "ext4_vfs_dir",
+	.id		= UCLASS_DIR,
+	.ops		= &ext4_dir_ops,
+	.priv_auto	= sizeof(struct ext4_dir_priv),
+};
