[Concept,18/21] fs: Add ISO 9660 U-Boot integration layer

Message ID 20260416165733.2923423-19-sjg@u-boot.org
State New
Headers
Series fs: Add ISO 9660 filesystem driver ported from Linux |

Commit Message

Simon Glass April 16, 2026, 4:57 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

Provide the U-Boot-specific integration that makes the Linux isofs
source files work within U-Boot's filesystem framework.

The compatibility header (isofs_uboot.h) follows the same approach
as ext4l: it stubs out Linux kernel infrastructure that is not
available in U-Boot, such as the filesystem-context mount API, CDROM
multi-session support and the mpage readahead layer. All printk() calls
are silenced since U-Boot does not filter by log level.

The isofs driver uses iget5_locked() with custom test/set callbacks
in support.c because ISO 9660 identifies inodes by their on-disk
block and offset rather than a simple inode number.

The interface layer (interface.c) bridges U-Boot's filesystem
conventions and the Linux VFS entry points. It handles mounting by
setting up the superblock and calling isofs_fill_super(), resolves paths
component by component using isofs_lookup(), and uses isofs_bread() to
read file data block by block. Directory listing uses the Linux
iterate_shared() callback with an actor that captures one entry per
call, matching the U-Boot opendir/readdir/closedir model.

The VFS driver (fs.c) exposes the filesystem through UCLASS_FS,
UCLASS_DIR and UCLASS_FILE so that it works with the mount/ls/cat
commands. The driver is also registered in the legacy filesystem table
for use with the load command.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 fs/fs_legacy.c         |  24 ++
 fs/isofs/Kconfig       |  19 ++
 fs/isofs/Makefile      |  13 +
 fs/isofs/fs.c          | 228 +++++++++++++++
 fs/isofs/inode.c       |   2 +-
 fs/isofs/interface.c   | 623 +++++++++++++++++++++++++++++++++++++++++
 fs/isofs/isofs_uboot.h | 308 ++++++++++++++++++++
 fs/isofs/support.c     |  85 ++++++
 include/fs_common.h    |   1 +
 include/isofs.h        |  93 ++++++
 include/linux/slab.h   |   2 +
 11 files changed, 1397 insertions(+), 1 deletion(-)
 create mode 100644 fs/isofs/Kconfig
 create mode 100644 fs/isofs/Makefile
 create mode 100644 fs/isofs/fs.c
 create mode 100644 fs/isofs/interface.c
 create mode 100644 fs/isofs/isofs_uboot.h
 create mode 100644 fs/isofs/support.c
 create mode 100644 include/isofs.h
  

Patch

diff --git a/fs/fs_legacy.c b/fs/fs_legacy.c
index 5286f07c659..5f7fd8e7ec5 100644
--- a/fs/fs_legacy.c
+++ b/fs/fs_legacy.c
@@ -20,6 +20,7 @@ 
 #include <ext4fs.h>
 #include <ext4l.h>
 #include <fat.h>
+#include <isofs.h>
 #include <fs_legacy.h>
 #include <sandboxfs.h>
 #include <semihostingfs.h>
@@ -295,6 +296,29 @@  static struct fstype_info fstypes[] = {
 		.statfs = ext4l_statfs_legacy,
 	},
 #endif
+#if CONFIG_IS_ENABLED(FS_ISOFS)
+	{
+		.fstype = FS_TYPE_ISO,
+		.name = "iso9660",
+		.null_dev_desc_ok = false,
+		.probe = isofs_probe,
+		.close = isofs_close,
+		.ls = isofs_ls,
+		.exists = isofs_exists,
+		.size = isofs_size,
+		.read = isofs_read,
+		.write = fs_write_unsupported,
+		.uuid = fs_uuid_unsupported,
+		.opendir = isofs_opendir,
+		.readdir = isofs_readdir,
+		.closedir = isofs_closedir,
+		.unlink = fs_unlink_unsupported,
+		.mkdir = fs_mkdir_unsupported,
+		.ln = fs_ln_unsupported,
+		.rename = fs_rename_unsupported,
+		.statfs = fs_statfs_unsupported,
+	},
+#endif
 #if IS_ENABLED(CONFIG_SANDBOX) && !IS_ENABLED(CONFIG_XPL_BUILD)
 	{
 		.fstype = FS_TYPE_SANDBOX,
diff --git a/fs/isofs/Kconfig b/fs/isofs/Kconfig
new file mode 100644
index 00000000000..4eda062796c
--- /dev/null
+++ b/fs/isofs/Kconfig
@@ -0,0 +1,19 @@ 
+config FS_ISOFS
+	bool "ISO 9660 filesystem support (Linux port)"
+	depends on FS
+	select FS_LINUX
+	help
+	  Support for reading ISO 9660 CD-ROM/DVD filesystems. This is a port
+	  of the Linux kernel isofs driver, providing full read-only access
+	  including Rock Ridge extensions for POSIX filenames and permissions.
+
+	  This driver uses the same porting approach as ext4l, keeping the
+	  Linux source files nearly identical.
+
+config JOLIET
+	bool "Microsoft Joliet CD-ROM extensions"
+	depends on FS_ISOFS
+	help
+	  Support for Microsoft Joliet CD-ROM extensions which provide
+	  Unicode filenames on ISO 9660 filesystems. This requires NLS
+	  (National Language Support) for character set conversion.
diff --git a/fs/isofs/Makefile b/fs/isofs/Makefile
new file mode 100644
index 00000000000..34b0ed75a83
--- /dev/null
+++ b/fs/isofs/Makefile
@@ -0,0 +1,13 @@ 
+# SPDX-License-Identifier: GPL-2.0
+
+# U-Boot interface and support
+obj-y := interface.o support.o
+
+# VFS layer integration
+obj-$(CONFIG_$(PHASE_)VFS) += fs.o
+
+# Core Linux isofs files (kept close to upstream)
+obj-y += namei.o inode.o dir.o util.o rock.o export.o
+
+# Optional extensions
+obj-$(CONFIG_JOLIET) += joliet.o
diff --git a/fs/isofs/fs.c b/fs/isofs/fs.c
new file mode 100644
index 00000000000..835b8a9e5a7
--- /dev/null
+++ b/fs/isofs/fs.c
@@ -0,0 +1,228 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * isofs filesystem driver for the VFS layer
+ *
+ * Wraps the Linux-ported isofs implementation to provide UCLASS_FS
+ * and UCLASS_DIR devices, following the same pattern as ext4l/fs.c.
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY	UCLASS_FS
+
+#include <dir.h>
+#include <dm.h>
+#include <file.h>
+#include <fs.h>
+#include <fs_legacy.h>
+#include <isofs.h>
+#include <iovec.h>
+#include <malloc.h>
+#include <vfs.h>
+#include <dm/device-internal.h>
+
+/**
+ * struct isofs_dir_priv - Private info for isofs directory devices
+ *
+ * @strm: Directory stream from isofs_opendir(), or NULL
+ */
+struct isofs_dir_priv {
+	struct fs_dir_stream *strm;
+};
+
+static int isofs_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 = isofs_probe(plat->desc, &plat->part);
+	if (ret)
+		return log_msg_ret("emp", ret);
+
+	uc_priv->mounted = true;
+
+	return 0;
+}
+
+static int isofs_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);
+
+	isofs_close();
+	uc_priv->mounted = false;
+
+	return 0;
+}
+
+static int isofs_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(isofs_vfs_dir), path, &dir);
+	if (ret)
+		return log_msg_ret("eld", ret);
+
+	*dirp = dir;
+
+	return 0;
+}
+
+static const struct fs_ops isofs_vfs_ops = {
+	.mount		= isofs_vfs_mount,
+	.unmount	= isofs_vfs_unmount,
+	.lookup_dir	= isofs_vfs_lookup_dir,
+};
+
+U_BOOT_DRIVER(isofs_fs) = {
+	.name	= "isofs_fs",
+	.id	= UCLASS_FS,
+	.ops	= &isofs_vfs_ops,
+};
+
+/* isofs directory driver */
+
+static int isofs_dir_open(struct udevice *dev, struct fs_dir_stream *strm)
+{
+	struct dir_uc_priv *uc_priv = dev_get_uclass_priv(dev);
+	struct isofs_dir_priv *priv = dev_get_priv(dev);
+	struct fs_dir_stream *iso_strm;
+	const char *path;
+	int ret;
+
+	path = *uc_priv->path ? uc_priv->path : "/";
+	ret = isofs_opendir(path, &iso_strm);
+	if (ret)
+		return log_msg_ret("edo", ret);
+
+	priv->strm = iso_strm;
+
+	return 0;
+}
+
+static int isofs_dir_read(struct udevice *dev, struct fs_dir_stream *strm,
+			  struct fs_dirent *dent)
+{
+	struct isofs_dir_priv *priv = dev_get_priv(dev);
+	struct fs_dirent *iso_dent;
+	int ret;
+
+	ret = isofs_readdir(priv->strm, &iso_dent);
+	if (ret)
+		return ret;
+
+	*dent = *iso_dent;
+
+	return 0;
+}
+
+static int isofs_dir_close(struct udevice *dev, struct fs_dir_stream *strm)
+{
+	struct isofs_dir_priv *priv = dev_get_priv(dev);
+
+	isofs_closedir(priv->strm);
+	priv->strm = NULL;
+
+	return 0;
+}
+
+/* isofs file driver */
+
+/**
+ * struct isofs_file_priv - Private info for isofs file devices
+ *
+ * @path: Full path within the isofs filesystem
+ */
+struct isofs_file_priv {
+	char path[FILE_MAX_PATH_LEN];
+};
+
+static ssize_t isofs_vfs_read_iter(struct udevice *dev, struct iov_iter *iter,
+				   loff_t pos)
+{
+	struct isofs_file_priv *priv = dev_get_priv(dev);
+	loff_t actual;
+	int ret;
+
+	ret = isofs_read(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;
+}
+
+static struct file_ops isofs_file_ops = {
+	.read_iter	= isofs_vfs_read_iter,
+};
+
+U_BOOT_DRIVER(isofs_vfs_file) = {
+	.name		= "isofs_vfs_file",
+	.id		= UCLASS_FILE,
+	.ops		= &isofs_file_ops,
+	.priv_auto	= sizeof(struct isofs_file_priv),
+};
+
+static int isofs_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);
+	char path[FILE_MAX_PATH_LEN];
+	struct isofs_file_priv *priv;
+	struct udevice *dev;
+	loff_t size = 0;
+	int ret;
+
+	/* ISO 9660 is read-only */
+	if (oflags != DIR_O_RDONLY)
+		return log_msg_ret("eow", -EROFS);
+
+	if (*uc_priv->path)
+		snprintf(path, sizeof(path), "%s/%s", uc_priv->path, leaf);
+	else
+		snprintf(path, sizeof(path), "/%s", leaf);
+
+	if (!isofs_exists(path))
+		return log_msg_ret("eoe", -ENOENT);
+	ret = isofs_size(path, &size);
+	if (ret)
+		return log_msg_ret("eos", ret);
+
+	ret = file_add_probe(dir, DM_DRIVER_REF(isofs_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 isofs_dir_vfs_ops = {
+	.open		= isofs_dir_open,
+	.read		= isofs_dir_read,
+	.close		= isofs_dir_close,
+	.open_file	= isofs_dir_open_file,
+};
+
+U_BOOT_DRIVER(isofs_vfs_dir) = {
+	.name		= "isofs_vfs_dir",
+	.id		= UCLASS_DIR,
+	.ops		= &isofs_dir_vfs_ops,
+	.priv_auto	= sizeof(struct isofs_dir_priv),
+};
diff --git a/fs/isofs/inode.c b/fs/isofs/inode.c
index 6d629252575..3d9e9301a9f 100644
--- a/fs/isofs/inode.c
+++ b/fs/isofs/inode.c
@@ -492,7 +492,7 @@  root_found:
 
 #ifdef CONFIG_JOLIET
 	if (joliet_level) {
-		char *p = opt->iocharset ? opt->iocharset : CONFIG_NLS_DEFAULT;
+		char *p = opt->iocharset ? opt->iocharset : CFG_NLS_DEFAULT;
 		if (strcmp(p, "utf8") != 0) {
 			sbi->s_nls_iocharset = opt->iocharset ?
 				load_nls(opt->iocharset) : load_nls_default();
diff --git a/fs/isofs/interface.c b/fs/isofs/interface.c
new file mode 100644
index 00000000000..6f07f9951f1
--- /dev/null
+++ b/fs/isofs/interface.c
@@ -0,0 +1,623 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * U-Boot interface for isofs filesystem (Linux port)
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ *
+ * This provides the interface between U-Boot's filesystem layer and
+ * the isofs driver (ISO 9660 CD-ROM filesystem).
+ */
+
+#include <blk.h>
+#include <fs.h>
+#include <fs_legacy.h>
+#include <part.h>
+#include <malloc.h>
+#include <linux/errno.h>
+#include <linux/types.h>
+
+#include "isofs.h"
+
+/**
+ * struct isofs_state - global mount state for the isofs driver
+ *
+ * @blk_dev: Block device descriptor
+ * @partition: Partition info
+ * @sb: Superblock pointer
+ * @mounted: Whether a filesystem is currently mounted
+ */
+static struct isofs_state {
+	struct blk_desc *blk_dev;
+	struct disk_partition partition;
+	struct super_block *sb;
+	bool mounted;
+} ifs;
+
+/**
+ * struct isofs_dir_actor_ctx - Context for directory listing callback
+ * @ctx: Base dir_context (must be first)
+ * @print: Whether to print entries
+ */
+struct isofs_dir_actor_ctx {
+	struct dir_context ctx;
+	bool print;
+};
+
+/**
+ * struct isofs_dir - isofs directory stream state
+ * @parent: base fs_dir_stream structure
+ * @dirent: directory entry to return to caller
+ * @dir_inode: pointer to directory inode
+ * @file: file structure for readdir
+ * @entry_found: flag set by actor when entry is captured
+ */
+struct isofs_dir {
+	struct fs_dir_stream parent;
+	struct fs_dirent dirent;
+	struct inode *dir_inode;
+	struct file file;
+	bool entry_found;
+};
+
+struct isofs_readdir_ctx {
+	struct dir_context ctx;
+	struct isofs_dir *dir;
+};
+
+/**
+ * isofs_probe() - Mount an ISO 9660 filesystem
+ * @fs_dev_desc: Block device descriptor
+ * @fs_partition: Partition info
+ * Return: 0 on success, negative on error
+ */
+int isofs_probe(struct blk_desc *fs_dev_desc,
+		struct disk_partition *fs_partition)
+{
+	struct isofs_options *opt;
+	struct super_block *sb;
+	struct fs_context fc;
+	loff_t part_offset;
+	u8 *buf;
+	int ret;
+
+	if (!fs_dev_desc)
+		return -EINVAL;
+
+	/* Close any previous mount */
+	if (ifs.sb)
+		isofs_close();
+
+	/* Initialise inode cache */
+	ret = isofs_init_inodecache();
+	if (ret)
+		return ret;
+
+	/* Allocate super_block */
+	sb = kzalloc(sizeof(*sb), GFP_KERNEL);
+	if (!sb) {
+		ret = -ENOMEM;
+		goto err_destroy_cache;
+	}
+	INIT_LIST_HEAD(&sb->s_inodes);
+
+	/* Allocate block_device */
+	sb->s_bdev = kzalloc(sizeof(*sb->s_bdev), GFP_KERNEL);
+	if (!sb->s_bdev) {
+		ret = -ENOMEM;
+		goto err_free_sb;
+	}
+
+	/* Initialise super_block fields */
+	sb->s_bdev->bd_super = sb;
+	sb->s_bdev->bd_blk = fs_dev_desc->bdev;
+	sb->s_bdev->bd_part_start = fs_partition ? fs_partition->start : 0;
+	sb->s_blocksize = ISOFS_BLOCK_SIZE;
+	sb->s_blocksize_bits = ISOFS_BLOCK_BITS;
+	sb->s_flags = SB_RDONLY;  /* ISO 9660 is always read-only */
+
+	/* Save device info */
+	ifs.blk_dev = fs_dev_desc;
+	if (fs_partition)
+		ifs.partition = *fs_partition;
+	else
+		memset(&ifs.partition, '\0', sizeof(ifs.partition));
+	ifs.mounted = true;
+
+	/* Read first sector to verify it's an ISO filesystem */
+	part_offset = fs_partition ?
+		(loff_t)fs_partition->start * fs_dev_desc->blksz : 0;
+
+	buf = malloc(ISOFS_BLOCK_SIZE);
+	if (!buf) {
+		ret = -ENOMEM;
+		goto err_free_bdev;
+	}
+
+	/*
+	 * Read system area block 16 where the primary volume descriptor
+	 * should be located.
+	 */
+	if (blk_read(fs_dev_desc->bdev,
+		     (part_offset + 16 * ISOFS_BLOCK_SIZE) / fs_dev_desc->blksz,
+		     ISOFS_BLOCK_SIZE / fs_dev_desc->blksz, buf) !=
+	    ISOFS_BLOCK_SIZE / fs_dev_desc->blksz) {
+		ret = -EIO;
+		goto err_free_buf;
+	}
+
+	/* Quick check for ISO standard ID "CD001" at offset 1 */
+	if (strncmp((char *)buf + 1, ISO_STANDARD_ID, 5)) {
+		ret = -EINVAL;
+		goto err_free_buf;
+	}
+	free(buf);
+
+	/* Set up mount options */
+	opt = kzalloc(sizeof(*opt), GFP_KERNEL);
+	if (!opt) {
+		ret = -ENOMEM;
+		goto err_free_bdev;
+	}
+
+	opt->map = 'n';
+	opt->rock = 1;
+	opt->joliet = 1;
+	opt->cruft = 0;
+	opt->hide = 0;
+	opt->showassoc = 0;
+	opt->check = 'u';
+	opt->nocompress = 0;
+	opt->blocksize = ISOFS_BLOCK_SIZE;
+	opt->fmode = ISOFS_INVALID_MODE;
+	opt->dmode = ISOFS_INVALID_MODE;
+	opt->uid_set = 0;
+	opt->gid_set = 0;
+	opt->gid = GLOBAL_ROOT_GID;
+	opt->uid = GLOBAL_ROOT_UID;
+	opt->iocharset = NULL;
+	opt->overriderockperm = 0;
+	opt->session = -1;
+	opt->sbsector = -1;
+
+	/* Set up fs_context */
+	memset(&fc, '\0', sizeof(fc));
+	fc.fs_private = opt;
+	fc.sb_flags = SB_RDONLY;
+
+	/* Mount the filesystem */
+	ret = isofs_fill_super(sb, &fc);
+	kfree(opt);
+	if (ret) {
+		printf("isofs: mount failed: %d\n", ret);
+		goto err_free_bdev;
+	}
+
+	ifs.sb = sb;
+	return 0;
+
+err_free_buf:
+	free(buf);
+err_free_bdev:
+	kfree(sb->s_bdev);
+err_free_sb:
+	kfree(sb);
+err_destroy_cache:
+	isofs_destroy_inodecache();
+	ifs.mounted = false;
+
+	return ret;
+}
+
+/**
+ * isofs_close() - Unmount the ISO 9660 filesystem
+ */
+void isofs_close(void)
+{
+	struct super_block *sb = ifs.sb;
+
+	if (!sb)
+		return;
+
+	/* Free all inodes */
+	isofs_free_inodes(sb);
+
+	/* Free root dentry */
+	kfree(sb->s_root);
+	sb->s_root = NULL;
+
+	/* Free sb_info */
+	kfree(sb->s_fs_info);
+	sb->s_fs_info = NULL;
+
+	/* Clear cached buffers for this device before freeing it */
+	bh_cache_clear(sb->s_bdev);
+
+	/* Free block device */
+	kfree(sb->s_bdev);
+	kfree(sb);
+
+	ifs.sb = NULL;
+
+	/* Destroy inode cache */
+	isofs_destroy_inodecache();
+
+	ifs.blk_dev = NULL;
+	ifs.mounted = false;
+}
+
+/**
+ * isofs_resolve_path() - Resolve a path to an inode
+ *
+ * This duplicates ext4l_resolve_path_internal(). Consider refactoring
+ * into a shared vfs_resolve_path() in linux_fs.c with a lookup callback.
+ *
+ * @path: Path to resolve
+ * @inodep: Output inode pointer
+ * Return: 0 on success, negative on error
+ */
+static int isofs_resolve_path(const char *path, struct inode **inodep)
+{
+	char *path_copy, *component, *next_component;
+	struct dentry *dentry, *result;
+	struct inode *dir;
+	int ret;
+
+	if (!ifs.mounted)
+		return -ENODEV;
+
+	dir = ifs.sb->s_root->d_inode;
+
+	if (!path || !*path || !strcmp(path, "/")) {
+		*inodep = dir;
+		return 0;
+	}
+
+	path_copy = strdup(path);
+	if (!path_copy)
+		return -ENOMEM;
+
+	component = path_copy;
+	if (*component == '/')
+		component++;
+
+	while (component && *component) {
+		next_component = strchr(component, '/');
+		if (next_component) {
+			*next_component = '\0';
+			next_component++;
+		}
+
+		if (!*component || !strcmp(component, ".")) {
+			component = next_component;
+			continue;
+		}
+
+		dentry = kzalloc(sizeof(*dentry), GFP_KERNEL);
+		if (!dentry) {
+			ret = -ENOMEM;
+			goto out_free;
+		}
+
+		dentry->d_name.name = component;
+		dentry->d_name.len = strlen(component);
+		dentry->d_sb = ifs.sb;
+		dentry->d_parent = NULL;
+
+		result = isofs_lookup(dir, dentry, 0);
+
+		if (IS_ERR(result)) {
+			ret = PTR_ERR(result);
+			kfree(dentry);
+			goto out_free;
+		}
+
+		if (result) {
+			if (!result->d_inode) {
+				if (result != dentry)
+					kfree(dentry);
+				kfree(result);
+				ret = -ENOENT;
+				goto out_free;
+			}
+			dir = result->d_inode;
+			if (result != dentry)
+				kfree(dentry);
+			kfree(result);
+		} else {
+			if (!dentry->d_inode) {
+				kfree(dentry);
+				ret = -ENOENT;
+				goto out_free;
+			}
+			dir = dentry->d_inode;
+			kfree(dentry);
+		}
+
+		if (!dir) {
+			ret = -ENOENT;
+			goto out_free;
+		}
+
+		/*
+		 * Follow symlinks (Rock Ridge). Read the link target and
+		 * resolve recursively. Limit depth to prevent loops.
+		 */
+		if (S_ISLNK(dir->i_mode)) {
+			/* Symlinks not yet implemented for isofs */
+			ret = -ENOENT;
+			goto out_free;
+		}
+
+		component = next_component;
+	}
+
+	*inodep = dir;
+	ret = 0;
+
+out_free:
+	free(path_copy);
+
+	return ret;
+}
+
+static int isofs_dir_actor(struct dir_context *ctx, const char *name,
+			   int namelen, loff_t offset, u64 ino,
+			   unsigned int d_type)
+{
+	char namebuf[256];
+
+	if (namelen >= sizeof(namebuf))
+		namelen = sizeof(namebuf) - 1;
+	memcpy(namebuf, name, namelen);
+	namebuf[namelen] = '\0';
+
+	if (d_type == DT_DIR)
+		printf("            %s/\n", namebuf);
+	else if (d_type == DT_LNK)
+		printf("    <SYM>   %s\n", namebuf);
+	else
+		printf("            %s\n", namebuf);
+
+	return 0;
+}
+
+int isofs_ls(const char *dirname)
+{
+	struct dir_context ctx;
+	struct inode *dir;
+	struct file file;
+	int ret;
+
+	ret = isofs_resolve_path(dirname, &dir);
+	if (ret)
+		return ret;
+
+	if (!S_ISDIR(dir->i_mode))
+		return -ENOTDIR;
+
+	memset(&file, '\0', sizeof(file));
+	file.f_inode = dir;
+	file.f_mapping = dir->i_mapping;
+
+	memset(&ctx, '\0', sizeof(ctx));
+	ctx.actor = isofs_dir_actor;
+
+	/* Call isofs_readdir via the file operations */
+	if (dir->i_fop && dir->i_fop->iterate_shared)
+		ret = dir->i_fop->iterate_shared(&file, &ctx);
+	else
+		ret = -ENOTDIR;
+
+	return ret;
+}
+
+int isofs_exists(const char *filename)
+{
+	struct inode *inode;
+
+	if (!filename)
+		return 0;
+
+	if (isofs_resolve_path(filename, &inode))
+		return 0;
+
+	return 1;
+}
+
+int isofs_size(const char *filename, loff_t *sizep)
+{
+	struct inode *inode;
+	int ret;
+
+	ret = isofs_resolve_path(filename, &inode);
+	if (ret)
+		return ret;
+
+	*sizep = inode->i_size;
+
+	return 0;
+}
+
+int isofs_read(const char *filename, void *buf, loff_t offset, loff_t len,
+	       loff_t *actread)
+{
+	uint copy_len, blk_off, blksize;
+	loff_t bytes_left, file_size;
+	struct buffer_head *bh;
+	struct inode *inode;
+	sector_t block;
+	char *dst;
+	int ret;
+
+	*actread = 0;
+
+	ret = isofs_resolve_path(filename, &inode);
+	if (ret) {
+		printf("** File not found %s **\n", filename);
+		return ret;
+	}
+
+	file_size = inode->i_size;
+	if (offset >= file_size)
+		return 0;
+
+	/* If len is 0, read the whole file from offset */
+	if (!len)
+		len = file_size - offset;
+
+	/* Clamp to file size */
+	if (offset + len > file_size)
+		len = file_size - offset;
+
+	blksize = inode->i_sb->s_blocksize;
+	bytes_left = len;
+	dst = buf;
+
+	while (bytes_left > 0) {
+		/* Calculate logical block number and offset within block */
+		block = offset / blksize;
+		blk_off = offset % blksize;
+
+		/* Read the block using isofs_bread (handles block mapping) */
+		bh = isofs_bread(inode, block);
+		if (!bh)
+			return -EIO;
+
+		/* Calculate how much to copy from this block */
+		copy_len = blksize - blk_off;
+		if (copy_len > bytes_left)
+			copy_len = bytes_left;
+
+		memcpy(dst, bh->b_data + blk_off, copy_len);
+		brelse(bh);
+
+		dst += copy_len;
+		offset += copy_len;
+		bytes_left -= copy_len;
+		*actread += copy_len;
+	}
+
+	return 0;
+}
+
+static int isofs_opendir_actor(struct dir_context *ctx, const char *name,
+			       int namelen, loff_t offset, u64 ino,
+			       unsigned int d_type)
+{
+	struct isofs_readdir_ctx *rctx;
+	struct dentry de, *result;
+	struct fs_dirent *dent;
+	struct isofs_dir *dir;
+
+	rctx = container_of(ctx, struct isofs_readdir_ctx, ctx);
+	dir = rctx->dir;
+	dent = &dir->dirent;
+
+	namelen = min(namelen, (int)FS_DIRENT_NAME_LEN - 1);
+	memcpy(dent->name, name, namelen);
+	dent->name[namelen] = '\0';
+
+	dent->size = 0;
+	dent->type = FS_DT_REG;
+
+	/* "." and ".." are always directories */
+	if (namelen <= 2 && name[0] == '.') {
+		dent->type = FS_DT_DIR;
+	} else {
+		/* Look up the entry to get its inode for size and type */
+		memset(&de, '\0', sizeof(de));
+		de.d_name.name = (const unsigned char *)dent->name;
+		de.d_name.len = namelen;
+		de.d_sb = dir->dir_inode->i_sb;
+
+		result = isofs_lookup(dir->dir_inode, &de, 0);
+		if (!IS_ERR_OR_NULL(result) && result->d_inode) {
+			dent->size = result->d_inode->i_size;
+			if (S_ISDIR(result->d_inode->i_mode))
+				dent->type = FS_DT_DIR;
+			else if (S_ISLNK(result->d_inode->i_mode))
+				dent->type = FS_DT_LNK;
+		} else if (de.d_inode) {
+			dent->size = de.d_inode->i_size;
+			if (S_ISDIR(de.d_inode->i_mode))
+				dent->type = FS_DT_DIR;
+			else if (S_ISLNK(de.d_inode->i_mode))
+				dent->type = FS_DT_LNK;
+		}
+	}
+
+	dir->entry_found = true;
+
+	/* Return non-zero to stop after one entry */
+	return 1;
+}
+
+int isofs_opendir(const char *filename, struct fs_dir_stream **dirsp)
+{
+	struct isofs_dir *dir;
+	struct inode *inode;
+	int ret;
+
+	if (!ifs.mounted)
+		return -ENODEV;
+
+	ret = isofs_resolve_path(filename, &inode);
+	if (ret)
+		return ret;
+
+	if (!S_ISDIR(inode->i_mode))
+		return -ENOTDIR;
+
+	dir = calloc(1, sizeof(*dir));
+	if (!dir)
+		return -ENOMEM;
+
+	dir->dir_inode = inode;
+	dir->file.f_inode = inode;
+	dir->file.f_mapping = inode->i_mapping;
+
+	*dirsp = (struct fs_dir_stream *)dir;
+
+	return 0;
+}
+
+int isofs_readdir(struct fs_dir_stream *dirs, struct fs_dirent **dentp)
+{
+	struct isofs_dir *dir = container_of(dirs, struct isofs_dir, parent);
+	struct isofs_readdir_ctx rctx;
+	int ret;
+
+	if (!ifs.mounted)
+		return -ENODEV;
+
+	memset(&dir->dirent, '\0', sizeof(dir->dirent));
+	dir->entry_found = false;
+
+	memset(&rctx, '\0', sizeof(rctx));
+	rctx.ctx.actor = isofs_opendir_actor;
+	rctx.ctx.pos = dir->file.f_pos;
+	rctx.dir = dir;
+
+	if (dir->dir_inode->i_fop && dir->dir_inode->i_fop->iterate_shared)
+		ret = dir->dir_inode->i_fop->iterate_shared(&dir->file,
+							    &rctx.ctx);
+	else
+		ret = -ENOTDIR;
+
+	dir->file.f_pos = rctx.ctx.pos;
+
+	if (ret < 0)
+		return ret;
+
+	if (!dir->entry_found)
+		return -ENOENT;
+
+	*dentp = &dir->dirent;
+
+	return 0;
+}
+
+void isofs_closedir(struct fs_dir_stream *dirs)
+{
+	free(dirs);
+}
diff --git a/fs/isofs/isofs_uboot.h b/fs/isofs/isofs_uboot.h
new file mode 100644
index 00000000000..b543ecb8999
--- /dev/null
+++ b/fs/isofs/isofs_uboot.h
@@ -0,0 +1,308 @@ 
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * U-Boot compatibility header for isofs filesystem
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ *
+ * This provides minimal definitions to allow Linux isofs code to compile
+ * in U-Boot. The isofs driver is read-only and much simpler than ext4,
+ * so the compatibility layer is lighter.
+ */
+
+#ifndef __ISOFS_UBOOT_H__
+#define __ISOFS_UBOOT_H__
+
+/*
+ * Suppress warnings for unused static functions and variables in Linux isofs
+ * source files.
+ */
+#pragma GCC diagnostic ignored "-Wunused-function"
+#pragma GCC diagnostic ignored "-Wunused-variable"
+
+/* U-Boot headers */
+#include <memalign.h>
+#include <vsprintf.h>
+
+/* Linux types - must come first */
+#include <linux/types.h>
+
+/* Linux headers */
+#include <asm/byteorder.h>
+#include <asm-generic/unaligned.h>
+#include <asm-generic/atomic.h>
+#include <linux/bitops.h>
+#include <linux/bug.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/fs.h>
+#include <linux/list.h>
+#include <linux/log2.h>
+#include <linux/nls.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/stat.h>
+#include <linux/string.h>
+#include <linux/time.h>
+#include <linux/buffer_head.h>
+#include <linux/cred.h>
+#include <linux/ctype.h>
+#include <linux/dcache.h>
+#include <linux/exportfs.h>
+#include <linux/fs_context.h>
+#include <linux/fs_parser.h>
+#include <linux/module.h>
+#include <linux/pagemap.h>
+#include <linux/seq_file.h>
+#include <linux/statfs.h>
+
+/*
+ * Suppress printk messages from Linux isofs code. In Linux these are
+ * filtered by log level; in U-Boot printk maps to printf for all levels.
+ * The isofs code uses printk only for debug and error messages that are
+ * not useful in U-Boot's context.
+ */
+#undef printk
+#define printk(fmt, ...)	({ if (0) printf(fmt, ##__VA_ARGS__); 0; })
+
+/* Page allocation wrappers */
+#ifndef __get_free_page
+#define __get_free_page(gfp) \
+	((unsigned long)memalign(PAGE_SIZE, PAGE_SIZE))
+#endif
+void free_page(unsigned long addr);
+
+#define alloc_page(gfp) \
+	((struct page *)memalign(PAGE_SIZE, PAGE_SIZE))
+#define __free_page(page)	free(page)
+
+/* Joliet support */
+#ifndef CFG_NLS_DEFAULT
+#define CFG_NLS_DEFAULT		"utf8"
+#endif
+
+/* CDROM support stubs - not available in U-Boot */
+struct cdrom_device_info;
+
+static inline struct cdrom_device_info *disk_to_cdi(void *disk)
+{
+	return NULL;
+}
+
+#define CDROM_LBA		0
+#define CDROM_DATA_TRACK	4
+
+struct cdrom_tocentry {
+	int cdte_track;
+	int cdte_format;
+	struct { int lba; } cdte_addr;
+	int cdte_ctrl;
+};
+
+struct cdrom_multisession {
+	int addr_format;
+	int xa_flag;
+	struct { int lba; } addr;
+};
+
+static inline int cdrom_read_tocentry(struct cdrom_device_info *cdi,
+				      struct cdrom_tocentry *te)
+{
+	return -ENODEV;
+}
+
+static inline int cdrom_multisession(struct cdrom_device_info *cdi,
+				     struct cdrom_multisession *ms)
+{
+	return -ENODEV;
+}
+
+/* set_default_d_op - no dentry ops needed in U-Boot */
+static inline void set_default_d_op(struct super_block *sb,
+				    const struct dentry_operations *ops) { }
+
+/* inode_nohighmem stub */
+static inline void inode_nohighmem(struct inode *inode) { }
+
+/* generic_ro_fops, page_symlink_inode_operations - defined in linux/fs.h */
+
+/* generic_setlease - lease management stub */
+struct file_lease;
+static inline int generic_setlease(struct file *fp, int arg,
+				   struct file_lease **flp, void **priv)
+{
+	return -EINVAL;
+}
+
+/* init_special_inode - override the macro from fs.h */
+#undef init_special_inode
+static inline void init_special_inode(struct inode *inode, umode_t mode,
+				      dev_t rdev)
+{
+	inode->i_mode = mode;
+	inode->i_rdev = rdev;
+}
+
+/* User namespace helpers for mount options */
+static inline uid_t from_kuid_munged(void *ns, kuid_t uid)
+{
+	return uid.val;
+}
+
+static inline gid_t from_kgid_munged(void *ns, kgid_t gid)
+{
+	return gid.val;
+}
+
+/* gendisk is forward-declared in linux/blkdev.h */
+
+/*
+ * mpage stubs - isofs uses mpage_read_folio/mpage_readahead for
+ * regular file I/O. We stub these since file reading goes through
+ * the interface layer directly.
+ */
+static inline int mpage_read_folio(struct folio *folio,
+				   get_block_t *get_block)
+{
+	return -EIO;
+}
+
+static inline void mpage_readahead(struct readahead_control *rac,
+				   get_block_t *get_block) { }
+
+/* generic_block_bmap stub */
+static inline sector_t generic_block_bmap(struct address_space *mapping,
+					  sector_t block,
+					  get_block_t *get_block)
+{
+	return 0;
+}
+
+/* huge_encode_dev - encode device for statfs */
+static inline u64 huge_encode_dev(dev_t dev)
+{
+	return dev;
+}
+
+/* u64_to_fsid - convert u64 to kernel_fsid_t */
+static inline __kernel_fsid_t u64_to_fsid(u64 v)
+{
+	__kernel_fsid_t fsid;
+
+	fsid.val[0] = v;
+	fsid.val[1] = v >> 32;
+	return fsid;
+}
+
+/* NAME_MAX */
+#ifndef NAME_MAX
+#define NAME_MAX	255
+#endif
+
+/* S_IXUGO */
+#ifndef S_IXUGO
+#define S_IXUGO		(S_IXUSR | S_IXGRP | S_IXOTH)
+#endif
+
+/* FILEID_INVALID for export.c */
+#ifndef FILEID_INVALID
+#define FILEID_INVALID	0xff
+#endif
+
+/* page_address - for page used as buffer */
+static inline void *page_address(void *page)
+{
+	return page;
+}
+
+/* folio_address is defined in linux/pagemap.h */
+/* folio_end_read is defined as a macro in linux/pagemap.h */
+
+/* Dentry operations - needed by isofs for case-insensitive lookup */
+struct dentry_operations {
+	int (*d_hash)(const struct dentry *dentry, struct qstr *qstr);
+	int (*d_compare)(const struct dentry *dentry, unsigned int len,
+			 const char *str, const struct qstr *name);
+};
+
+/* Name hash functions for dentry operations */
+static inline unsigned long init_name_hash(const struct dentry *dentry)
+{
+	return 0;
+}
+
+static inline unsigned long partial_name_hash(unsigned long c,
+					      unsigned long prevhash)
+{
+	return (prevhash + (c << 4) + (c >> 4)) * 11;
+}
+
+static inline unsigned long end_name_hash(unsigned long hash)
+{
+	return (unsigned int)hash;
+}
+
+static inline unsigned int full_name_hash(const struct dentry *dentry,
+					  const char *name, unsigned int len)
+{
+	unsigned long hash = init_name_hash(dentry);
+
+	while (len--)
+		hash = partial_name_hash((unsigned char)*name++, hash);
+	return end_name_hash(hash);
+}
+
+/* generic_file_llseek - stub for directory operations */
+static inline loff_t generic_file_llseek(struct file *file, loff_t offset,
+					 int whence)
+{
+	return offset;
+}
+
+/* isofs_close - forward declaration for interface.c */
+void isofs_close(void);
+
+/*
+ * iget5_locked - isofs uses custom test/set callbacks for inode lookup.
+ * Implemented in support.c.
+ */
+struct inode *iget5_locked(struct super_block *sb, unsigned long hashval,
+			   int (*test)(struct inode *, void *),
+			   int (*set)(struct inode *, void *), void *data);
+
+/* iget_failed - override the macro from fs.h */
+#undef iget_failed
+void iget_failed(struct inode *inode);
+
+/*
+ * d_obtain_alias - override the stub macro from dcache.h.
+ * Needed by export.c for NFS file handle lookup.
+ */
+#undef d_obtain_alias
+static inline struct dentry *d_obtain_alias(struct inode *inode)
+{
+	struct dentry *d;
+
+	if (IS_ERR(inode))
+		return ERR_CAST(inode);
+	d = kzalloc(sizeof(*d), GFP_KERNEL);
+	if (!d) {
+		iput(inode);
+		return ERR_PTR(-ENOMEM);
+	}
+	d->d_inode = inode;
+	return d;
+}
+
+/* dir_emit, dir_emit_dot, dir_emit_dotdot - defined in linux/fs.h */
+
+/* sb_bread, brelse, etc. are now in fs/linux_fs.c */
+struct buffer_head *sb_bread(struct super_block *sb, sector_t block);
+void bh_cache_clear(struct block_device *bdev);
+void isofs_free_inodes(struct super_block *sb);
+
+/* isofs inode.c exports */
+int isofs_fill_super(struct super_block *s, struct fs_context *fc);
+int isofs_init_inodecache(void);
+void isofs_destroy_inodecache(void);
+
+#endif /* __ISOFS_UBOOT_H__ */
diff --git a/fs/isofs/support.c b/fs/isofs/support.c
new file mode 100644
index 00000000000..1ac986d882e
--- /dev/null
+++ b/fs/isofs/support.c
@@ -0,0 +1,85 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Internal support functions for isofs filesystem
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ *
+ * This provides isofs-specific support functions: iget5_locked() with
+ * custom test/set callbacks, and inode management.
+ *
+ * Common VFS functions (buffer cache, block I/O, brelse, dir_emit, etc.)
+ * are provided by fs/linux_fs.c.
+ */
+
+#include <malloc.h>
+#include <linux/errno.h>
+#include <linux/types.h>
+
+#include "isofs.h"
+
+/**
+ * iget5_locked() - Get an inode with custom test/set callbacks
+ * @sb: Superblock
+ * @hashval: Hash value (unused in U-Boot)
+ * @test: Test function to check if inode matches
+ * @set: Set function to initialise inode data
+ * @data: Opaque data passed to test/set
+ *
+ * In U-Boot we always allocate a new inode since we don't cache them.
+ */
+struct inode *iget5_locked(struct super_block *sb, unsigned long hashval,
+			   int (*test)(struct inode *, void *),
+			   int (*set)(struct inode *, void *), void *data)
+{
+	struct iso_inode_info *ei;
+	struct inode *inode;
+
+	ei = kzalloc(sizeof(*ei), GFP_KERNEL);
+	if (!ei)
+		return NULL;
+
+	inode = &ei->vfs_inode;
+	memset(inode, '\0', sizeof(*inode));
+	inode->i_sb = sb;
+	inode->i_blkbits = sb->s_blocksize_bits;
+	inode->i_state = I_NEW;
+	inode->i_count.counter = 1;
+	inode->i_mapping = &inode->i_data;
+	inode->i_data.host = inode;
+	INIT_LIST_HEAD(&inode->i_sb_list);
+
+	if (set)
+		set(inode, data);
+
+	list_add(&inode->i_sb_list, &sb->s_inodes);
+
+	return inode;
+}
+
+/**
+ * iget_failed() - Mark inode as failed and release
+ * @inode: Inode that failed to initialise
+ */
+void iget_failed(struct inode *inode)
+{
+	if (!inode)
+		return;
+	list_del_init(&inode->i_sb_list);
+	kfree(ISOFS_I(inode));
+}
+
+/**
+ * isofs_free_inodes() - Free all inodes on the superblock list
+ * @sb: Superblock whose inodes to free
+ */
+void isofs_free_inodes(struct super_block *sb)
+{
+	while (!list_empty(&sb->s_inodes)) {
+		struct inode *inode;
+
+		inode = list_first_entry(&sb->s_inodes,
+					 struct inode, i_sb_list);
+		list_del_init(&inode->i_sb_list);
+		kfree(ISOFS_I(inode));
+	}
+}
diff --git a/include/fs_common.h b/include/fs_common.h
index 396e36fa8ee..68c4f5c9a8f 100644
--- a/include/fs_common.h
+++ b/include/fs_common.h
@@ -22,6 +22,7 @@  enum fs_type_t {
 	FS_TYPE_SEMIHOSTING,
 	FS_TYPE_EXFAT,
 	FS_TYPE_VIRTIO,
+	FS_TYPE_ISO,
 };
 
 /*
diff --git a/include/isofs.h b/include/isofs.h
new file mode 100644
index 00000000000..e9e02011bc0
--- /dev/null
+++ b/include/isofs.h
@@ -0,0 +1,93 @@ 
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Public interface for the isofs (ISO 9660) filesystem driver
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ */
+
+#ifndef __ISOFS_H__
+#define __ISOFS_H__
+
+#include <blk.h>
+#include <part.h>
+#include <fs.h>
+
+/**
+ * isofs_probe() - Mount an ISO 9660 filesystem
+ *
+ * @fs_dev_desc: Block device descriptor
+ * @fs_partition: Partition info (can be NULL for whole disk)
+ * Return: 0 on success, negative on error
+ */
+int isofs_probe(struct blk_desc *fs_dev_desc,
+		struct disk_partition *fs_partition);
+
+/**
+ * isofs_close() - Unmount the ISO 9660 filesystem
+ */
+void isofs_close(void);
+
+/**
+ * isofs_ls() - List files in a directory
+ *
+ * @dirname: Directory path
+ * Return: 0 on success, negative on error
+ */
+int isofs_ls(const char *dirname);
+
+/**
+ * isofs_exists() - Check if a file exists
+ *
+ * @filename: File path
+ * Return: 1 if file exists, 0 if not
+ */
+int isofs_exists(const char *filename);
+
+/**
+ * isofs_size() - Get the size of a file
+ *
+ * @filename: File path
+ * @sizep: Output file size
+ * Return: 0 on success, negative on error
+ */
+int isofs_size(const char *filename, loff_t *sizep);
+
+/**
+ * isofs_read() - Read data from a file
+ *
+ * @filename: File path
+ * @buf: Output buffer
+ * @offset: Byte offset to read from
+ * @len: Number of bytes to read (0 = entire file)
+ * @actread: Output actual bytes read
+ * Return: 0 on success, negative on error
+ */
+int isofs_read(const char *filename, void *buf, loff_t offset, loff_t len,
+	       loff_t *actread);
+
+/**
+ * isofs_opendir() - Open a directory for iteration
+ *
+ * @filename: Directory path
+ * @dirsp: Output directory stream pointer
+ * Return: 0 on success, negative on error
+ */
+int isofs_opendir(const char *filename, struct fs_dir_stream **dirsp);
+
+/**
+ * isofs_readdir() - Read next directory entry
+ *
+ * @dirs: Directory stream from isofs_opendir()
+ * @dentp: Output directory entry pointer
+ * Return: 0 on success, -ENOENT at end, negative on error
+ */
+int isofs_readdir(struct fs_dir_stream *dirs, struct fs_dirent **dentp);
+
+/**
+ * isofs_closedir() - Close a directory stream
+ *
+ * @dirs: Directory stream to close
+ */
+void isofs_closedir(struct fs_dir_stream *dirs);
+
+#endif /* __ISOFS_H__ */
diff --git a/include/linux/slab.h b/include/linux/slab.h
index 6722450a5cc..866db0230eb 100644
--- a/include/linux/slab.h
+++ b/include/linux/slab.h
@@ -72,6 +72,8 @@  static inline void *kzalloc(size_t size, gfp_t flags)
 	return kmalloc(size, flags | __GFP_ZERO);
 }
 
+#define kzalloc_obj(obj, ...)	kzalloc(sizeof(obj), GFP_KERNEL)
+
 static inline void *kmalloc_array(size_t n, size_t size, gfp_t flags)
 {
 	if (size != 0 && n > SIZE_MAX / size)