[PATCH] erofs-utils: lib: fix potential shift UB in z_erofs lclusterbits handling
Utkal Singh
singhutkal015 at gmail.com
Fri Mar 6 05:13:39 AEDT 2026
z_lclusterbits is computed from on-disk h_clusterbits without an upper
bound check: blkszbits + (h_clusterbits & 15). While blkszbits is
validated in erofs_read_superblock(), EROFS_MAX_BLOCK_SIZE can be
overridden at compile time (e.g. 64K pages on ARM64), making
blkszbits up to 16 and z_lclusterbits up to 31.
Several places in z_erofs_map_blocks_ext() and related functions use
'1 << lclusterbits' where the literal 1 has type int (32-bit signed).
Per C11 6.5.7p4, left-shifting a signed positive value such that the
result is not representable is undefined behavior.
Fix this by:
- Adding a validation check rejecting z_lclusterbits > 30 as
filesystem corruption, since no valid EROFS image uses logical
clusters larger than 1 GiB.
- Changing all '1 << lclusterbits' to '1U << lclusterbits' to
use unsigned arithmetic, matching the kernel EROFS driver style.
This hardens the z_erofs metadata parsing path against crafted images.
Signed-off-by: Utkal Singh <singhutkal015 at gmail.com>
---
lib/zmap.c | 23 +++++++++++++++--------
1 file changed, 15 insertions(+), 8 deletions(-)
diff --git a/lib/zmap.c b/lib/zmap.c
index 0e7af4e..42e982d 100644
--- a/lib/zmap.c
+++ b/lib/zmap.c
@@ -45,7 +45,7 @@ static int z_erofs_load_full_lcluster(struct z_erofs_maprecorder *m,
advise = le16_to_cpu(di->di_advise);
m->type = advise & Z_EROFS_LI_LCLUSTER_TYPE_MASK;
if (m->type == Z_EROFS_LCLUSTER_TYPE_NONHEAD) {
- m->clusterofs = 1 << vi->z_lclusterbits;
+ m->clusterofs = 1U << vi->z_lclusterbits;
m->delta[0] = le16_to_cpu(di->di_u.delta[0]);
if (m->delta[0] & Z_EROFS_LI_D0_CBLKCNT) {
if (!(vi->z_advise & (Z_EROFS_ADVISE_BIG_PCLUSTER_1 |
@@ -60,7 +60,7 @@ static int z_erofs_load_full_lcluster(struct z_erofs_maprecorder *m,
} else {
m->partialref = !!(advise & Z_EROFS_LI_PARTIAL_REF);
m->clusterofs = le16_to_cpu(di->di_clusterofs);
- if (m->clusterofs >= 1 << vi->z_lclusterbits) {
+ if (m->clusterofs >= 1U << vi->z_lclusterbits) {
DBG_BUGON(1);
return -EFSCORRUPTED;
}
@@ -168,7 +168,7 @@ static int z_erofs_load_compact_lcluster(struct z_erofs_maprecorder *m,
lo = decode_compactedbits(lobits, in, encodebits * i, &type);
m->type = type;
if (type == Z_EROFS_LCLUSTER_TYPE_NONHEAD) {
- m->clusterofs = 1 << lclusterbits;
+ m->clusterofs = 1U << lclusterbits;
/* figure out lookahead_distance: delta[1] if needed */
if (lookahead)
@@ -423,7 +423,7 @@ static int z_erofs_map_blocks_fo(struct erofs_inode *vi,
return 0;
}
initial_lcn = ofs >> lclusterbits;
- endoff = ofs & ((1 << lclusterbits) - 1);
+ endoff = ofs & ((1U << lclusterbits) - 1);
err = z_erofs_load_lcluster_from_disk(&m, initial_lcn, false);
if (err)
@@ -561,12 +561,12 @@ static int z_erofs_map_blocks_ext(struct erofs_inode *vi,
pos += sizeof(__le64);
lstart = 0;
} else {
- lstart = round_down(map->m_la, 1 << vi->z_lclusterbits);
+ lstart = round_down(map->m_la, 1U << vi->z_lclusterbits);
pos += (lstart >> vi->z_lclusterbits) * recsz;
pa = EROFS_NULL_ADDR;
}
- for (; lstart <= map->m_la; lstart += 1 << vi->z_lclusterbits) {
+ for (; lstart <= map->m_la; lstart += 1U << vi->z_lclusterbits) {
ext = erofs_read_metabuf(&map->buf, sbi, pos, in_mbox);
if (IS_ERR(ext))
return PTR_ERR(ext);
@@ -579,9 +579,9 @@ static int z_erofs_map_blocks_ext(struct erofs_inode *vi,
}
pos += recsz;
}
- last = (lstart >= round_up(lend, 1 << vi->z_lclusterbits));
+ last = (lstart >= round_up(lend, 1U << vi->z_lclusterbits));
lend = min(lstart, lend);
- lstart -= 1 << vi->z_lclusterbits;
+ lstart -= 1U << vi->z_lclusterbits;
} else {
lstart = lend;
for (l = 0, r = vi->z_extents; l < r; ) {
@@ -673,6 +673,13 @@ static int z_erofs_fill_inode_lazy(struct erofs_inode *vi)
vi->z_advise = le16_to_cpu(h->h_advise);
vi->z_lclusterbits = sbi->blkszbits + (h->h_clusterbits & 15);
+
+ if (vi->z_lclusterbits > 30) {
+ erofs_err("invalid lclusterbits %u of nid %llu",
+ vi->z_lclusterbits, vi->nid | 0ULL);
+ err = -EFSCORRUPTED;
+ goto out_put_metabuf;
+ }
if (vi->datalayout == EROFS_INODE_COMPRESSED_FULL &&
(vi->z_advise & Z_EROFS_ADVISE_EXTENTS)) {
vi->z_extents = le32_to_cpu(h->h_extents_lo) |
--
2.43.0
More information about the Linux-erofs
mailing list