erofs: separate plain and compressed filesystems formally

The EROFS on-disk format uses a tiny, plain metadata design that
prioritizes performance and minimizes complex inconsistencies against
common writable disk filesystems (almost all serious metadata
inconsistency cannot happen in well-designed immutable filesystems like
EROFS). EROFS deliberately avoids artificial design flaws to eliminate
serious security risks from untrusted remote sources by design,
although human-made implementation bugs can still happen sometimes.

Currently, there is no strict check to prevent compressed inodes,
especially LZ4-compressed inodes, from being read in plain filesystems.

Starting with erofs-utils 1.0 and Linux 5.3, LZ4_0PADDING sb feature
is automatically enabled for LZ4-compressed EROFS images to support
in-place decompression. Furthermore, since Linux 5.4 LTS is no longer
supported, we no longer need to handle ancient LZ4-compressed EROFS
images generated by erofs-utils prior to 1.0.

To formally distinguish different filesystem types for improved
security:

 - Use the presence of LZ4_0PADDING or a non-zero
   `dsb->u1.lz4_max_distance` as a marker for compressed filesystems
   containing LZ4-compressed inodes only;

 - For other algorithms, use `dsb->u1.available_compr_algs` bitmap.

Note: LZ4_0PADDING has been supported since Linux 5.4 (the first formal
kernel version), so exposing it via sysfs is no longer necessary and is
now deprecated (but remain it for five more years until 2031):

  `dsb->u1` has been strictly non-zero for all EROFS images containing
  compressed inodes starting with erofs-utils v1.3 and it is actually
  a much better marker for compressed filesystems.

Signed-off-by: Gao Xiang <hsiangkao@linux.alibaba.com>
This commit is contained in:
Gao Xiang 2026-01-29 10:41:25 +08:00
parent 72558e2bed
commit 7cef3c8341
7 changed files with 39 additions and 45 deletions

View File

@ -3,9 +3,9 @@ Date: November 2021
Contact: "Huang Jianan" <huangjianan@oppo.com> Contact: "Huang Jianan" <huangjianan@oppo.com>
Description: Shows all enabled kernel features. Description: Shows all enabled kernel features.
Supported features: Supported features:
zero_padding, compr_cfgs, big_pcluster, chunked_file, compr_cfgs, big_pcluster, chunked_file, device_table,
device_table, compr_head2, sb_chksum, ztailpacking, compr_head2, sb_chksum, ztailpacking, dedupe, fragments,
dedupe, fragments, 48bit, metabox. 48bit, metabox.
What: /sys/fs/erofs/<disk>/sync_decompress What: /sys/fs/erofs/<disk>/sync_decompress
Date: November 2021 Date: November 2021

View File

@ -34,7 +34,10 @@ static int z_erofs_load_lz4_config(struct super_block *sb,
} }
} else { } else {
distance = le16_to_cpu(dsb->u1.lz4_max_distance); distance = le16_to_cpu(dsb->u1.lz4_max_distance);
if (!distance && !erofs_sb_has_lz4_0padding(sbi))
return 0;
sbi->lz4.max_pclusterblks = 1; sbi->lz4.max_pclusterblks = 1;
sbi->available_compr_algs = 1 << Z_EROFS_COMPRESSION_LZ4;
} }
sbi->lz4.max_distance_pages = distance ? sbi->lz4.max_distance_pages = distance ?
@ -198,7 +201,6 @@ const char *z_erofs_fixup_insize(struct z_erofs_decompress_req *rq,
static const char *__z_erofs_lz4_decompress(struct z_erofs_decompress_req *rq, static const char *__z_erofs_lz4_decompress(struct z_erofs_decompress_req *rq,
u8 *dst) u8 *dst)
{ {
bool zeropadded = erofs_sb_has_zero_padding(EROFS_SB(rq->sb));
bool may_inplace = false; bool may_inplace = false;
unsigned int inputmargin; unsigned int inputmargin;
u8 *out, *headpage, *src; u8 *out, *headpage, *src;
@ -206,18 +208,15 @@ static const char *__z_erofs_lz4_decompress(struct z_erofs_decompress_req *rq,
int ret, maptype; int ret, maptype;
headpage = kmap_local_page(*rq->in); headpage = kmap_local_page(*rq->in);
/* LZ4 decompression inplace is only safe if zero_padding is enabled */ reason = z_erofs_fixup_insize(rq, headpage + rq->pageofs_in,
if (zeropadded) { min_t(unsigned int, rq->inputsize,
reason = z_erofs_fixup_insize(rq, headpage + rq->pageofs_in, rq->sb->s_blocksize - rq->pageofs_in));
min_t(unsigned int, rq->inputsize, if (reason) {
rq->sb->s_blocksize - rq->pageofs_in)); kunmap_local(headpage);
if (reason) { return reason;
kunmap_local(headpage);
return reason;
}
may_inplace = !((rq->pageofs_in + rq->inputsize) &
(rq->sb->s_blocksize - 1));
} }
may_inplace = !((rq->pageofs_in + rq->inputsize) &
(rq->sb->s_blocksize - 1));
inputmargin = rq->pageofs_in; inputmargin = rq->pageofs_in;
src = z_erofs_lz4_handle_overlap(rq, headpage, dst, &inputmargin, src = z_erofs_lz4_handle_overlap(rq, headpage, dst, &inputmargin,
@ -226,8 +225,7 @@ static const char *__z_erofs_lz4_decompress(struct z_erofs_decompress_req *rq,
return ERR_CAST(src); return ERR_CAST(src);
out = dst + rq->pageofs_out; out = dst + rq->pageofs_out;
/* legacy format could compress extra data in a pcluster. */ if (rq->partial_decoding)
if (rq->partial_decoding || !zeropadded)
ret = LZ4_decompress_safe_partial(src + inputmargin, out, ret = LZ4_decompress_safe_partial(src + inputmargin, out,
rq->inputsize, rq->outputsize, rq->outputsize); rq->inputsize, rq->outputsize, rq->outputsize);
else else
@ -454,10 +452,8 @@ int z_erofs_parse_cfgs(struct super_block *sb, struct erofs_super_block *dsb)
erofs_off_t offset; erofs_off_t offset;
int size, ret = 0; int size, ret = 0;
if (!erofs_sb_has_compr_cfgs(sbi)) { if (!erofs_sb_has_compr_cfgs(sbi))
sbi->available_compr_algs = 1 << Z_EROFS_COMPRESSION_LZ4;
return z_erofs_load_lz4_config(sb, dsb, NULL, 0); return z_erofs_load_lz4_config(sb, dsb, NULL, 0);
}
algs = le16_to_cpu(dsb->u1.available_compr_algs); algs = le16_to_cpu(dsb->u1.available_compr_algs);
sbi->available_compr_algs = algs; sbi->available_compr_algs = algs;

View File

@ -23,7 +23,7 @@
* Any bits that aren't in EROFS_ALL_FEATURE_INCOMPAT should * Any bits that aren't in EROFS_ALL_FEATURE_INCOMPAT should
* be incompatible with this kernel version. * be incompatible with this kernel version.
*/ */
#define EROFS_FEATURE_INCOMPAT_ZERO_PADDING 0x00000001 #define EROFS_FEATURE_INCOMPAT_LZ4_0PADDING 0x00000001
#define EROFS_FEATURE_INCOMPAT_COMPR_CFGS 0x00000002 #define EROFS_FEATURE_INCOMPAT_COMPR_CFGS 0x00000002
#define EROFS_FEATURE_INCOMPAT_BIG_PCLUSTER 0x00000002 #define EROFS_FEATURE_INCOMPAT_BIG_PCLUSTER 0x00000002
#define EROFS_FEATURE_INCOMPAT_CHUNKED_FILE 0x00000004 #define EROFS_FEATURE_INCOMPAT_CHUNKED_FILE 0x00000004

View File

@ -183,11 +183,17 @@ static int erofs_read_inode(struct inode *inode)
goto err_out; goto err_out;
} }
if (erofs_inode_is_data_compressed(vi->datalayout)) if (!erofs_inode_is_data_compressed(vi->datalayout)) {
inode->i_blocks = le32_to_cpu(copied.i_u.blocks_lo) <<
(sb->s_blocksize_bits - 9);
else
inode->i_blocks = round_up(inode->i_size, sb->s_blocksize) >> 9; inode->i_blocks = round_up(inode->i_size, sb->s_blocksize) >> 9;
} else if (!IS_ENABLED(CONFIG_EROFS_FS_ZIP) || !sbi->available_compr_algs) {
erofs_err(sb, "compressed inode (nid %llu) is invalid in a plain filesystem",
vi->nid);
err = -EFSCORRUPTED;
goto err_out;
} else {
inode->i_blocks = le32_to_cpu(copied.i_u.blocks_lo) <<
(sb->s_blocksize_bits - 9);
}
if (vi->datalayout == EROFS_INODE_CHUNK_BASED) { if (vi->datalayout == EROFS_INODE_CHUNK_BASED) {
/* fill chunked inode summary info */ /* fill chunked inode summary info */

View File

@ -114,7 +114,6 @@ struct erofs_sb_info {
unsigned int sync_decompress; /* strategy for sync decompression */ unsigned int sync_decompress; /* strategy for sync decompression */
unsigned int shrinker_run_no; unsigned int shrinker_run_no;
u16 available_compr_algs;
/* pseudo inode to manage cached pages */ /* pseudo inode to manage cached pages */
struct inode *managed_cache; struct inode *managed_cache;
@ -154,6 +153,7 @@ struct erofs_sb_info {
char *volume_name; char *volume_name;
u32 feature_compat; u32 feature_compat;
u32 feature_incompat; u32 feature_incompat;
u16 available_compr_algs;
/* sysfs support */ /* sysfs support */
struct kobject s_kobj; /* /sys/fs/erofs/<devname> */ struct kobject s_kobj; /* /sys/fs/erofs/<devname> */
@ -221,7 +221,7 @@ static inline bool erofs_sb_has_##name(struct erofs_sb_info *sbi) \
return sbi->feature_##compat & EROFS_FEATURE_##feature; \ return sbi->feature_##compat & EROFS_FEATURE_##feature; \
} }
EROFS_FEATURE_FUNCS(zero_padding, incompat, INCOMPAT_ZERO_PADDING) EROFS_FEATURE_FUNCS(lz4_0padding, incompat, INCOMPAT_LZ4_0PADDING)
EROFS_FEATURE_FUNCS(compr_cfgs, incompat, INCOMPAT_COMPR_CFGS) EROFS_FEATURE_FUNCS(compr_cfgs, incompat, INCOMPAT_COMPR_CFGS)
EROFS_FEATURE_FUNCS(big_pcluster, incompat, INCOMPAT_BIG_PCLUSTER) EROFS_FEATURE_FUNCS(big_pcluster, incompat, INCOMPAT_BIG_PCLUSTER)
EROFS_FEATURE_FUNCS(chunked_file, incompat, INCOMPAT_CHUNKED_FILE) EROFS_FEATURE_FUNCS(chunked_file, incompat, INCOMPAT_CHUNKED_FILE)
@ -530,7 +530,6 @@ void z_erofs_put_gbuf(void *ptr);
int z_erofs_gbuf_growsize(unsigned int nrpages); int z_erofs_gbuf_growsize(unsigned int nrpages);
int __init z_erofs_gbuf_init(void); int __init z_erofs_gbuf_init(void);
void z_erofs_gbuf_exit(void); void z_erofs_gbuf_exit(void);
int z_erofs_parse_cfgs(struct super_block *sb, struct erofs_super_block *dsb);
#else #else
static inline void erofs_shrinker_register(struct super_block *sb) {} static inline void erofs_shrinker_register(struct super_block *sb) {}
static inline void erofs_shrinker_unregister(struct super_block *sb) {} static inline void erofs_shrinker_unregister(struct super_block *sb) {}
@ -540,6 +539,7 @@ static inline int z_erofs_init_subsystem(void) { return 0; }
static inline void z_erofs_exit_subsystem(void) {} static inline void z_erofs_exit_subsystem(void) {}
static inline int z_erofs_init_super(struct super_block *sb) { return 0; } static inline int z_erofs_init_super(struct super_block *sb) { return 0; }
#endif /* !CONFIG_EROFS_FS_ZIP */ #endif /* !CONFIG_EROFS_FS_ZIP */
int z_erofs_parse_cfgs(struct super_block *sb, struct erofs_super_block *dsb);
#ifdef CONFIG_EROFS_FS_BACKED_BY_FILE #ifdef CONFIG_EROFS_FS_BACKED_BY_FILE
struct bio *erofs_fileio_bio_alloc(struct erofs_map_dev *mdev); struct bio *erofs_fileio_bio_alloc(struct erofs_map_dev *mdev);

View File

@ -122,18 +122,6 @@ void *erofs_read_metadata(struct super_block *sb, struct erofs_buf *buf,
return buffer; return buffer;
} }
#ifndef CONFIG_EROFS_FS_ZIP
static int z_erofs_parse_cfgs(struct super_block *sb,
struct erofs_super_block *dsb)
{
if (!dsb->u1.available_compr_algs)
return 0;
erofs_err(sb, "compression disabled, unable to mount compressed EROFS");
return -EOPNOTSUPP;
}
#endif
static int erofs_init_device(struct erofs_buf *buf, struct super_block *sb, static int erofs_init_device(struct erofs_buf *buf, struct super_block *sb,
struct erofs_device_info *dif, erofs_off_t *pos) struct erofs_device_info *dif, erofs_off_t *pos)
{ {
@ -363,10 +351,16 @@ static int erofs_read_superblock(struct super_block *sb)
} }
} }
/* parse on-disk compression configurations */ if (IS_ENABLED(CONFIG_EROFS_FS_ZIP)) {
ret = z_erofs_parse_cfgs(sb, dsb); ret = z_erofs_parse_cfgs(sb, dsb);
if (ret < 0) if (ret < 0)
goto out;
} else if (dsb->u1.available_compr_algs ||
erofs_sb_has_lz4_0padding(sbi)) {
erofs_err(sb, "compression disabled, unable to mount compressed EROFS");
ret = -EOPNOTSUPP;
goto out; goto out;
}
ret = erofs_scan_devices(sb, dsb); ret = erofs_scan_devices(sb, dsb);

View File

@ -86,7 +86,6 @@ static struct attribute *erofs_attrs[] = {
ATTRIBUTE_GROUPS(erofs); ATTRIBUTE_GROUPS(erofs);
/* Features this copy of erofs supports */ /* Features this copy of erofs supports */
EROFS_ATTR_FEATURE(zero_padding);
EROFS_ATTR_FEATURE(compr_cfgs); EROFS_ATTR_FEATURE(compr_cfgs);
EROFS_ATTR_FEATURE(big_pcluster); EROFS_ATTR_FEATURE(big_pcluster);
EROFS_ATTR_FEATURE(chunked_file); EROFS_ATTR_FEATURE(chunked_file);
@ -100,7 +99,6 @@ EROFS_ATTR_FEATURE(48bit);
EROFS_ATTR_FEATURE(metabox); EROFS_ATTR_FEATURE(metabox);
static struct attribute *erofs_feat_attrs[] = { static struct attribute *erofs_feat_attrs[] = {
ATTR_LIST(zero_padding),
ATTR_LIST(compr_cfgs), ATTR_LIST(compr_cfgs),
ATTR_LIST(big_pcluster), ATTR_LIST(big_pcluster),
ATTR_LIST(chunked_file), ATTR_LIST(chunked_file),