[Concept,19/26] fs: ext4l: Add buffer_head I/O infrastructure

Message ID 20251222115639.700578-20-sjg@u-boot.org
State New
Headers
Series fs: ext4l: Add support for mounting ext4 filesystems (part G) |

Commit Message

Simon Glass Dec. 22, 2025, 11:56 a.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add support.c with buffer_head I/O infrastructure for ext4l:
- Buffer cache for caching buffer_heads across lookups
- Buffer allocation/free functions
- Block I/O functions (sb_getblk, sb_bread, brelse, submit_bh, bh_read)

This keeps interface.c focused on the U-Boot filesystem layer interface.

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 fs/ext4l/Makefile           |   2 +-
 fs/ext4l/ext4_uboot.h       |  36 ++-
 fs/ext4l/interface.c        |  60 ++++-
 fs/ext4l/stub.c             |  18 +-
 fs/ext4l/support.c          | 428 ++++++++++++++++++++++++++++++++++++
 include/linux/buffer_head.h |   8 +-
 6 files changed, 528 insertions(+), 24 deletions(-)
 create mode 100644 fs/ext4l/support.c
  

Patch

diff --git a/fs/ext4l/Makefile b/fs/ext4l/Makefile
index e99b900ca6d..7bb843c3fad 100644
--- a/fs/ext4l/Makefile
+++ b/fs/ext4l/Makefile
@@ -3,7 +3,7 @@ 
 # Makefile for the ext4l filesystem (Linux port)
 #
 
-obj-y := interface.o stub.o
+obj-y := interface.o support.o stub.o
 
 obj-y	+= balloc.o bitmap.o block_validity.o dir.o ext4_jbd2.o extents.o \
 		extents_status.o file.o fsmap.o fsync.o hash.o ialloc.o \
diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h
index 7c65aa8567e..dfa362389a8 100644
--- a/fs/ext4l/ext4_uboot.h
+++ b/fs/ext4l/ext4_uboot.h
@@ -313,9 +313,9 @@  extern struct user_namespace init_user_ns;
 #define __bforget(bh)			do { } while (0)
 #define mark_buffer_dirty_inode(bh, i)	do { } while (0)
 #define mark_buffer_dirty(bh)		do { } while (0)
-#define lock_buffer(bh)			do { } while (0)
-#define unlock_buffer(bh)		do { } while (0)
-#define sb_getblk(sb, block)		((struct buffer_head *)NULL)
+#define lock_buffer(bh)			set_buffer_locked(bh)
+#define unlock_buffer(bh)		clear_buffer_locked(bh)
+struct buffer_head *sb_getblk(struct super_block *sb, sector_t block);
 #define test_clear_buffer_dirty(bh)	({ (void)(bh); 0; })
 #define wait_on_bit_io(addr, bit, mode)	do { (void)(addr); (void)(bit); (void)(mode); } while (0)
 
@@ -1026,7 +1026,7 @@  static inline unsigned long memweight(const void *ptr, size_t bytes)
 #define rwsem_is_locked(sem)		(1)
 
 /* Buffer operations */
-#define sb_getblk_gfp(sb, blk, gfp)	((struct buffer_head *)NULL)
+#define sb_getblk_gfp(sb, blk, gfp)	sb_getblk((sb), (blk))
 #define bh_uptodate_or_lock(bh)		(1)
 /* ext4_read_bh is stubbed in interface.c */
 
@@ -1870,7 +1870,14 @@  struct file_system_type {
 #define FS_ALLOW_IDMAP		32
 
 /* Buffer read sync */
-#define end_buffer_read_sync	NULL
+static inline void end_buffer_read_sync(struct buffer_head *bh, int uptodate)
+{
+	if (uptodate)
+		set_buffer_uptodate(bh);
+	else
+		clear_buffer_uptodate(bh);
+	unlock_buffer(bh);
+}
 #define REQ_OP_READ		0
 
 /* Superblock flags */
@@ -2377,7 +2384,7 @@  void dquot_free_block(struct inode *inode, loff_t nr);
 
 /* Block device file operations - stubs */
 #define set_blocksize(f, size)		({ (void)(f); (void)(size); 0; })
-#define __bread(bdev, block, size)	({ (void)(bdev); (void)(block); (void)(size); (struct buffer_head *)NULL; })
+struct buffer_head *__bread(struct block_device *bdev, sector_t block, unsigned size);
 
 /* Trace stubs for super.c */
 #define trace_ext4_sync_fs(sb, wait)	do { (void)(sb); (void)(wait); } while (0)
@@ -2823,7 +2830,16 @@  struct wait_bit_entry {
 #define filemap_fdatawait_range_keep_errors(m, s, e) \
 	({ (void)(m); (void)(s); (void)(e); 0; })
 #define crc32_be(crc, p, len)		crc32(crc, p, len)
-#define free_buffer_head(bh)		kfree(bh)
+void free_buffer_head(struct buffer_head *bh);
+
+/* ext4l support functions (support.c) */
+void bh_cache_clear(void);
+int ext4l_read_block(sector_t block, size_t size, void *buffer);
+
+/* ext4l interface functions (interface.c) */
+struct blk_desc *ext4l_get_blk_dev(void);
+struct disk_partition *ext4l_get_partition(void);
+
 #define sb_is_blkdev_sb(sb)		({ (void)(sb); 0; })
 
 /* DEFINE_WAIT stub - creates a wait queue entry */
@@ -2850,7 +2866,7 @@  struct wait_bit_entry {
 #define trace_jbd2_lock_buffer_stall(...)	do { } while (0)
 
 /* JBD2 journal.c stubs */
-#define alloc_buffer_head(gfp)		((struct buffer_head *)kzalloc(sizeof(struct buffer_head), gfp))
+struct buffer_head *alloc_buffer_head(gfp_t gfp_mask);
 #define __getblk(bdev, block, size)	({ (void)(bdev); (void)(block); (void)(size); (struct buffer_head *)NULL; })
 #define bmap(inode, block)		({ (void)(inode); (void)(block); 0; })
 #define trace_jbd2_update_log_tail(j, t, b, f) \
@@ -2897,8 +2913,8 @@  loff_t seq_lseek(struct file *f, loff_t o, int w);
 	do { (void)(j); (void)(f); } while (0)
 
 /* Block device operations for journal.c */
-#define bh_read(bh, flags)		({ (void)(bh); (void)(flags); 0; })
-#define bh_read_nowait(bh, flags)	do { (void)(bh); (void)(flags); } while (0)
+int bh_read(struct buffer_head *bh, int flags);
+#define bh_read_nowait(bh, flags)	bh_read(bh, flags)
 #define bh_readahead_batch(n, bhs, f)	do { (void)(n); (void)(bhs); (void)(f); } while (0)
 #define truncate_inode_pages_range(m, s, e) \
 	do { (void)(m); (void)(s); (void)(e); } while (0)
diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c
index 4652f8c835f..e1458ea6bfe 100644
--- a/fs/ext4l/interface.c
+++ b/fs/ext4l/interface.c
@@ -5,13 +5,13 @@ 
  * Copyright 2025 Canonical Ltd
  * Written by Simon Glass <simon.glass@canonical.com>
  *
- * This provides the minimal interface between U-Boot and the ext4l driver.
+ * This provides the interface between U-Boot's filesystem layer and
+ * the ext4l driver.
  */
 
 #include <blk.h>
 #include <part.h>
 #include <malloc.h>
-#include <asm/byteorder.h>
 #include <linux/errno.h>
 #include <linux/jbd2.h>
 #include <linux/types.h>
@@ -23,6 +23,58 @@ 
 static struct blk_desc *ext4l_dev_desc;
 static struct disk_partition ext4l_part;
 
+/* Global block device tracking for buffer I/O */
+static struct blk_desc *ext4l_blk_dev;
+static struct disk_partition ext4l_partition;
+static int ext4l_mounted;
+
+/**
+ * ext4l_get_blk_dev() - Get the current block device
+ * Return: Block device descriptor or NULL if not mounted
+ */
+struct blk_desc *ext4l_get_blk_dev(void)
+{
+	if (!ext4l_mounted)
+		return NULL;
+	return ext4l_blk_dev;
+}
+
+/**
+ * ext4l_get_partition() - Get the current partition info
+ * Return: Partition info pointer
+ */
+struct disk_partition *ext4l_get_partition(void)
+{
+	return &ext4l_partition;
+}
+
+/**
+ * ext4l_set_blk_dev() - Set the block device for ext4l operations
+ * @blk_dev: Block device descriptor
+ * @partition: Partition info (can be NULL for whole disk)
+ */
+void ext4l_set_blk_dev(struct blk_desc *blk_dev, struct disk_partition *partition)
+{
+	ext4l_blk_dev = blk_dev;
+	if (partition)
+		memcpy(&ext4l_partition, partition, sizeof(struct disk_partition));
+	else
+		memset(&ext4l_partition, 0, sizeof(struct disk_partition));
+	ext4l_mounted = 1;
+}
+
+/**
+ * ext4l_clear_blk_dev() - Clear block device (unmount)
+ */
+void ext4l_clear_blk_dev(void)
+{
+	/* Clear buffer cache before unmounting */
+	bh_cache_clear();
+
+	ext4l_blk_dev = NULL;
+	ext4l_mounted = 0;
+}
+
 int ext4l_probe(struct blk_desc *fs_dev_desc,
 		struct disk_partition *fs_partition)
 {
@@ -37,6 +89,9 @@  int ext4l_probe(struct blk_desc *fs_dev_desc,
 	if (!fs_dev_desc)
 		return -EINVAL;
 
+	/* Set up block device for buffer I/O */
+	ext4l_set_blk_dev(fs_dev_desc, fs_partition);
+
 	/* Initialise journal subsystem if enabled */
 	if (IS_ENABLED(CONFIG_EXT4_JOURNAL)) {
 		ret = jbd2_journal_init_global();
@@ -162,5 +217,6 @@  err_exit_es:
 
 void ext4l_close(void)
 {
+	ext4l_clear_blk_dev();
 	ext4l_dev_desc = NULL;
 }
diff --git a/fs/ext4l/stub.c b/fs/ext4l/stub.c
index e13685bf8fa..eff54bb540c 100644
--- a/fs/ext4l/stub.c
+++ b/fs/ext4l/stub.c
@@ -309,20 +309,14 @@  void *bdev_file_open_by_dev(dev_t dev, int flags, void *holder,
 	return ERR_PTR(-ENODEV);
 }
 
-struct buffer_head *bdev_getblk(struct block_device *bdev, sector_t block,
-				unsigned int size, gfp_t gfp)
-{
-	return NULL;
-}
+/* bdev_getblk implemented in interface.c */
 
 int trylock_buffer(struct buffer_head *bh)
 {
 	return 1;
 }
 
-void submit_bh(int op, struct buffer_head *bh)
-{
-}
+/* submit_bh implemented in interface.c */
 
 /* NFS export stubs */
 struct dentry *generic_fh_to_parent(struct super_block *sb, struct fid *fid,
@@ -519,6 +513,14 @@  void fs_put_dax(void *dax, void *holder)
 /* Block size */
 int sb_set_blocksize(struct super_block *sb, int size)
 {
+	/* Validate block size */
+	if (size != 1024 && size != 2048 && size != 4096)
+		return 0;
+
+	/* Update superblock fields */
+	sb->s_blocksize = size;
+	sb->s_blocksize_bits = ffs(size) - 1;
+
 	return size;
 }
 
diff --git a/fs/ext4l/support.c b/fs/ext4l/support.c
new file mode 100644
index 00000000000..0765065a99f
--- /dev/null
+++ b/fs/ext4l/support.c
@@ -0,0 +1,428 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Internal support functions for ext4l filesystem
+ *
+ * Copyright 2025 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ *
+ * This provides internal support functions for the ext4l driver,
+ * including buffer_head I/O and buffer cache.
+ */
+
+#include <blk.h>
+#include <part.h>
+#include <malloc.h>
+#include <linux/errno.h>
+#include <linux/types.h>
+
+#include "ext4_uboot.h"
+#include "ext4.h"
+
+/*
+ * Buffer cache implementation
+ *
+ * Linux's sb_getblk() returns the same buffer_head for the same block number,
+ * allowing flags like BH_Verified, BH_Uptodate, etc. to persist across calls.
+ * This is critical for ext4's bitmap validation which sets buffer_verified()
+ * and expects it to remain set on subsequent lookups.
+ */
+#define BH_CACHE_BITS	8
+#define BH_CACHE_SIZE	(1 << BH_CACHE_BITS)
+#define BH_CACHE_MASK	(BH_CACHE_SIZE - 1)
+
+struct bh_cache_entry {
+	struct buffer_head *bh;
+	struct bh_cache_entry *next;
+};
+
+static struct bh_cache_entry *bh_cache[BH_CACHE_SIZE];
+
+static inline unsigned int bh_cache_hash(sector_t block)
+{
+	return (unsigned int)(block & BH_CACHE_MASK);
+}
+
+/**
+ * bh_cache_lookup() - Look up a buffer in the cache
+ * @block: Block number to look up
+ * @size: Expected block size
+ * Return: Buffer head if found with matching size, NULL otherwise
+ */
+static struct buffer_head *bh_cache_lookup(sector_t block, size_t size)
+{
+	unsigned int hash = bh_cache_hash(block);
+	struct bh_cache_entry *entry;
+
+	for (entry = bh_cache[hash]; entry; entry = entry->next) {
+		if (entry->bh && entry->bh->b_blocknr == block &&
+		    entry->bh->b_size == size) {
+			atomic_inc(&entry->bh->b_count);
+			return entry->bh;
+		}
+	}
+	return NULL;
+}
+
+/**
+ * bh_cache_insert() - Insert a buffer into the cache
+ * @bh: Buffer head to insert
+ */
+static void bh_cache_insert(struct buffer_head *bh)
+{
+	unsigned int hash = bh_cache_hash(bh->b_blocknr);
+	struct bh_cache_entry *entry;
+
+	/* Check if already in cache */
+	for (entry = bh_cache[hash]; entry; entry = entry->next) {
+		if (entry->bh && entry->bh->b_blocknr == bh->b_blocknr)
+			return;  /* Already cached */
+	}
+
+	entry = malloc(sizeof(struct bh_cache_entry));
+	if (!entry)
+		return;  /* Silently fail - cache is optional */
+
+	entry->bh = bh;
+	entry->next = bh_cache[hash];
+	bh_cache[hash] = entry;
+
+	/* Add a reference to keep the buffer alive in cache */
+	atomic_inc(&bh->b_count);
+}
+
+/**
+ * bh_cache_clear() - Clear the entire buffer cache
+ *
+ * Called on unmount to free all cached buffers.
+ */
+void bh_cache_clear(void)
+{
+	int i;
+	struct bh_cache_entry *entry, *next;
+
+	for (i = 0; i < BH_CACHE_SIZE; i++) {
+		for (entry = bh_cache[i]; entry; entry = next) {
+			next = entry->next;
+			if (entry->bh) {
+				/* Release the cache's reference */
+				if (atomic_dec_and_test(&entry->bh->b_count))
+					free_buffer_head(entry->bh);
+			}
+			free(entry);
+		}
+		bh_cache[i] = NULL;
+	}
+}
+
+/**
+ * alloc_buffer_head() - Allocate a buffer_head structure
+ * @gfp_mask: Allocation flags (ignored in U-Boot)
+ * Return: Pointer to buffer_head or NULL on error
+ */
+struct buffer_head *alloc_buffer_head(gfp_t gfp_mask)
+{
+	struct buffer_head *bh;
+
+	bh = malloc(sizeof(struct buffer_head));
+	if (!bh)
+		return NULL;
+
+	memset(bh, 0, sizeof(struct buffer_head));
+
+	/* Note: b_data will be allocated when needed by read functions */
+	atomic_set(&bh->b_count, 1);
+
+	return bh;
+}
+
+/**
+ * alloc_buffer_head_with_data() - Allocate a buffer_head with data buffer
+ * @size: Size of the data buffer to allocate
+ * Return: Pointer to buffer_head or NULL on error
+ */
+static struct buffer_head *alloc_buffer_head_with_data(size_t size)
+{
+	struct buffer_head *bh;
+
+	bh = malloc(sizeof(struct buffer_head));
+	if (!bh)
+		return NULL;
+
+	memset(bh, 0, sizeof(struct buffer_head));
+
+	bh->b_data = malloc(size);
+	if (!bh->b_data) {
+		free(bh);
+		return NULL;
+	}
+
+	bh->b_size = size;
+	/* Allocate a folio for kmap_local_folio() to work */
+	bh->b_folio = malloc(sizeof(struct folio));
+	if (bh->b_folio) {
+		memset(bh->b_folio, 0, sizeof(struct folio));
+		bh->b_folio->data = bh->b_data;
+	}
+	atomic_set(&bh->b_count, 1);
+	/* Mark that this buffer owns its b_data and should free it */
+	set_bit(BH_OwnsData, &bh->b_state);
+
+	return bh;
+}
+
+/**
+ * free_buffer_head() - Free a buffer_head
+ * @bh: Buffer head to free
+ *
+ * Only free b_data if BH_OwnsData is set. Shadow buffers created by
+ * jbd2_journal_write_metadata_buffer() share b_data with the original
+ * buffer and should not free it.
+ */
+void free_buffer_head(struct buffer_head *bh)
+{
+	if (!bh)
+		return;
+
+	/* Only free b_data if this buffer owns it */
+	if (bh->b_data && test_bit(BH_OwnsData, &bh->b_state))
+		free(bh->b_data);
+	if (bh->b_folio)
+		free(bh->b_folio);
+	free(bh);
+}
+
+/**
+ * ext4l_read_block() - Read a block from the block device
+ * @block: Block number (filesystem block, not sector)
+ * @size: Block size in bytes
+ * @buffer: Destination buffer
+ * Return: 0 on success, negative on error
+ */
+int ext4l_read_block(sector_t block, size_t size, void *buffer)
+{
+	struct blk_desc *blk_dev;
+	struct disk_partition *part;
+	lbaint_t sector;
+	lbaint_t sector_count;
+	unsigned long n;
+
+	blk_dev = ext4l_get_blk_dev();
+	part = ext4l_get_partition();
+	if (!blk_dev)
+		return -EIO;
+
+	/* Convert block to sector */
+	sector = (block * size) / blk_dev->blksz + part->start;
+	sector_count = size / blk_dev->blksz;
+
+	if (sector_count == 0)
+		sector_count = 1;
+
+	n = blk_dread(blk_dev, sector, sector_count, buffer);
+	if (n != sector_count)
+		return -EIO;
+
+	return 0;
+}
+
+/**
+ * sb_getblk() - Get a buffer, using cache if available
+ * @sb: Super block
+ * @block: Block number
+ * Return: Buffer head or NULL on error
+ */
+struct buffer_head *sb_getblk(struct super_block *sb, sector_t block)
+{
+	struct buffer_head *bh;
+
+	if (!sb)
+		return NULL;
+
+	/* Check cache first - must match block number AND size */
+	bh = bh_cache_lookup(block, sb->s_blocksize);
+	if (bh)
+		return bh;
+
+	/* Allocate new buffer */
+	bh = alloc_buffer_head_with_data(sb->s_blocksize);
+	if (!bh)
+		return NULL;
+
+	bh->b_blocknr = block;
+	bh->b_bdev = sb->s_bdev;
+	bh->b_size = sb->s_blocksize;
+
+	/* Don't read - just allocate with zeroed data */
+	memset(bh->b_data, '\0', bh->b_size);
+
+	/* Add to cache */
+	bh_cache_insert(bh);
+
+	return bh;
+}
+
+/**
+ * sb_bread() - Read a block via super_block
+ * @sb: Super block
+ * @block: Block number to read
+ * Return: Buffer head or NULL on error
+ */
+struct buffer_head *sb_bread(struct super_block *sb, sector_t block)
+{
+	struct buffer_head *bh;
+	int ret;
+
+	if (!sb)
+		return NULL;
+
+	bh = sb_getblk(sb, block);
+	if (!bh)
+		return NULL;
+
+	/* If buffer is already up-to-date, return it without re-reading */
+	if (buffer_uptodate(bh))
+		return bh;
+
+	bh->b_blocknr = block;
+	bh->b_bdev = sb->s_bdev;
+	bh->b_size = sb->s_blocksize;
+
+	ret = ext4l_read_block(block, sb->s_blocksize, bh->b_data);
+	if (ret) {
+		brelse(bh);
+		return NULL;
+	}
+
+	/* Mark buffer as up-to-date */
+	set_buffer_uptodate(bh);
+
+	return bh;
+}
+
+/**
+ * brelse() - Release a buffer_head
+ * @bh: Buffer head to release
+ */
+void brelse(struct buffer_head *bh)
+{
+	if (!bh)
+		return;
+
+	if (atomic_dec_and_test(&bh->b_count))
+		free_buffer_head(bh);
+}
+
+/**
+ * __brelse() - Release a buffer_head (alternate API)
+ * @bh: Buffer head to release
+ */
+void __brelse(struct buffer_head *bh)
+{
+	brelse(bh);
+}
+
+/**
+ * bdev_getblk() - Get buffer via block_device
+ * @bdev: Block device
+ * @block: Block number
+ * @size: Block size
+ * @gfp: Allocation flags
+ * Return: Buffer head or NULL
+ */
+struct buffer_head *bdev_getblk(struct block_device *bdev, sector_t block,
+				unsigned size, gfp_t gfp)
+{
+	struct buffer_head *bh;
+
+	/* Check cache first - must match block number AND size */
+	bh = bh_cache_lookup(block, size);
+	if (bh)
+		return bh;
+
+	bh = alloc_buffer_head_with_data(size);
+	if (!bh)
+		return NULL;
+
+	bh->b_blocknr = block;
+	bh->b_bdev = bdev;
+	bh->b_size = size;
+
+	/* Don't read - just allocate with zeroed data */
+	memset(bh->b_data, 0, bh->b_size);
+
+	/* Add to cache */
+	bh_cache_insert(bh);
+
+	return bh;
+}
+
+/**
+ * __bread() - Read a block via block_device
+ * @bdev: Block device
+ * @block: Block number to read
+ * @size: Block size
+ * Return: Buffer head or NULL on error
+ */
+struct buffer_head *__bread(struct block_device *bdev, sector_t block,
+			    unsigned size)
+{
+	struct buffer_head *bh;
+	int ret;
+
+	bh = alloc_buffer_head_with_data(size);
+	if (!bh)
+		return NULL;
+
+	bh->b_blocknr = block;
+	bh->b_bdev = bdev;
+	bh->b_size = size;
+
+	ret = ext4l_read_block(block, size, bh->b_data);
+	if (ret) {
+		free_buffer_head(bh);
+		return NULL;
+	}
+
+	/* Mark buffer as up-to-date */
+	set_bit(BH_Uptodate, &bh->b_state);
+
+	return bh;
+}
+
+/**
+ * submit_bh() - Submit a buffer_head for I/O
+ * @op: Operation (REQ_OP_READ, REQ_OP_WRITE, etc.)
+ * @bh: Buffer head to submit
+ */
+void submit_bh(int op, struct buffer_head *bh)
+{
+	int ret;
+	int op_type = op & 0xff;  /* Mask out flags, keep operation type */
+
+	if (op_type == REQ_OP_READ) {
+		ret = ext4l_read_block(bh->b_blocknr, bh->b_size, bh->b_data);
+		if (ret) {
+			clear_buffer_uptodate(bh);
+			return;
+		}
+		set_buffer_uptodate(bh);
+	} else if (op_type == REQ_OP_WRITE) {
+		/* Write support not implemented yet */
+		clear_buffer_uptodate(bh);
+	}
+}
+
+/**
+ * bh_read() - Read a buffer_head from disk
+ * @bh: Buffer head to read
+ * @flags: Read flags
+ * Return: 0 on success, negative on error
+ */
+int bh_read(struct buffer_head *bh, int flags)
+{
+	if (!bh || !bh->b_data)
+		return -EINVAL;
+
+	submit_bh(REQ_OP_READ | flags, bh);
+	return buffer_uptodate(bh) ? 0 : -EIO;
+}
diff --git a/include/linux/buffer_head.h b/include/linux/buffer_head.h
index b7596a74108..0f8f5b6caf1 100644
--- a/include/linux/buffer_head.h
+++ b/include/linux/buffer_head.h
@@ -40,6 +40,8 @@  enum bh_state_bits {
 	BH_PrivateStart,/* not a state bit, but the first bit available
 			 * for private allocation by other entities
 			 */
+	/* U-Boot specific: marks buffer owns b_data and should free it */
+	BH_OwnsData = BH_PrivateStart,
 };
 
 #define MAX_BUF_PER_PAGE (PAGE_SIZE / 512)
@@ -176,8 +178,8 @@  static inline void put_bh(struct buffer_head *bh)
 	atomic_dec(&bh->b_count);
 }
 
-/* Stubs for U-Boot */
-#define brelse(bh)		do { if (bh) put_bh(bh); } while (0)
-#define __brelse(bh)		do { put_bh(bh); } while (0)
+/* Buffer release functions - implemented in ext4l/interface.c */
+void brelse(struct buffer_head *bh);
+void __brelse(struct buffer_head *bh);
 
 #endif /* _LINUX_BUFFER_HEAD_H */