From 0b207d2b9a8a51b4a67ff5f451e1e46fb59bf1a2 Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Thu, 6 Aug 2026 21:43:39 -0400 Subject: [PATCH] btrfs: props: add per-inode compression level support Setting a per-file compression level is an often requested feature, and the btrfs.compression property has silently accepted level suffixes ("zstd:9") since compression types were added to it: the property validator only matches the algorithm name prefix, the verbatim string is stored in the xattr and returned by getxattr, and everything after the algorithm name is ignored when the value is parsed into the in-memory compression type. All of the pieces needed to honor the level already exist: the level is a per-call argument down the whole compression path, per-inode levels are already implemented for the defrag ioctl (defrag_compress_level), and btrfs_compress_str2level() already parses and clamps ":level" suffixes for the mount options. Wire the property path up to them: * Cache the parsed level in a new btrfs_inode::prop_compress_level, with 0 meaning no level was specified, in which case the level from the mount options is used as before. The field is signed to allow negative (realtime) zstd levels. * Parse an optional ":level" suffix in prop_compression_apply(). Values stored by old kernels were never validated, so an unparseable suffix falls back to the default level rather than making the inode's properties fail to load. Levels for lzo parse and clamp to nothing, matching commit 6db1df415d73 ("btrfs: accept and ignore compression level for lzo"). * Use the level in compress_file_range() when compression is selected by the property. The defrag ioctl retains precedence. * Regenerate the canonical "type:level" string with a new helper, btrfs_prop_compression_extract(), so that directory inheritance propagates the level to new inodes, and so that FS_IOC_SETFLAGS, which rewrites the property when setting FS_COMPR_FL, preserves the level instead of truncating the value to the bare algorithm name. The prop_handler extract hook itself is unchanged: it still returns a static string, and the inheritance loop regenerates the leveled value only for the compression property. There is no disk format change: the level lives in the already-existing xattr value string. Note that levels stored by old kernels (which were accepted but ignored) become effective after this change. Signed-off-by: Zygo Blaxell Assisted-by: Claude:claude-fable-5 --- fs/btrfs/btrfs_inode.h | 7 +++++ fs/btrfs/inode.c | 2 ++ fs/btrfs/ioctl.c | 7 +++-- fs/btrfs/props.c | 71 ++++++++++++++++++++++++++++++++++++++++++ fs/btrfs/props.h | 3 ++ 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h index b6b46f951f11a..6db2e853f3db7 100644 --- a/fs/btrfs/btrfs_inode.h +++ b/fs/btrfs/btrfs_inode.h @@ -140,6 +140,13 @@ struct btrfs_inode { /* Cached value of inode property 'compression'. */ u8 prop_compress; + /* + * Cached compression level from the 'compression' property. 0 means + * no level was specified, in which case the mount option level is + * used. + */ + s8 prop_compress_level; + /* * Force compression on the file using the defrag ioctl, could be * different from prop_compress and takes precedence if set. diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index cca9840bee896..e02b4b6ea9beb 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -969,6 +969,8 @@ again: compress_level = inode->defrag_compress_level; } else if (inode->prop_compress) { compress_type = inode->prop_compress; + if (inode->prop_compress_level) + compress_level = inode->prop_compress_level; } /* Compression level is applied here. */ diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 2cd89e88332c1..4161012fd2b40 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -255,6 +255,7 @@ int btrfs_fileattr_set(struct mnt_idmap *idmap, int ret; const char *comp = NULL; u32 old_inode_flags; + char comp_buf[16]; u32 inode_flags; bool prop_set = false; @@ -382,8 +383,10 @@ int btrfs_fileattr_set(struct mnt_idmap *idmap, inode_flags |= BTRFS_INODE_COMPRESS; inode_flags &= ~BTRFS_INODE_NOCOMPRESS; - comp = btrfs_compress_type2str(inode->prop_compress); - if (!comp || comp[0] == 0) + if (btrfs_prop_compression_extract(inode, comp_buf, + sizeof(comp_buf)) > 0) + comp = comp_buf; + else comp = btrfs_compress_type2str(fs_info->compress_type); if (!comp || comp[0] == 0) comp = btrfs_compress_type2str(BTRFS_COMPRESS_ZLIB); diff --git a/fs/btrfs/props.c b/fs/btrfs/props.c index 8d25e517fa76a..4e3588580b8f9 100644 --- a/fs/btrfs/props.c +++ b/fs/btrfs/props.c @@ -307,11 +307,15 @@ static int prop_compression_apply(struct btrfs_inode *inode, const char *value, struct btrfs_fs_info *fs_info = inode->root->fs_info; int type; + size_t type_len; + int level = 0; + /* Reset to defaults */ if (len == 0) { inode->flags &= ~BTRFS_INODE_COMPRESS; inode->flags &= ~BTRFS_INODE_NOCOMPRESS; inode->prop_compress = BTRFS_COMPRESS_NONE; + inode->prop_compress_level = 0; return 0; } @@ -321,29 +325,86 @@ static int prop_compression_apply(struct btrfs_inode *inode, const char *value, inode->flags |= BTRFS_INODE_NOCOMPRESS; inode->flags &= ~BTRFS_INODE_COMPRESS; inode->prop_compress = BTRFS_COMPRESS_NONE; + inode->prop_compress_level = 0; return 0; } if (!strncmp("lzo", value, 3)) { type = BTRFS_COMPRESS_LZO; + type_len = 3; btrfs_set_fs_incompat(fs_info, COMPRESS_LZO); } else if (!strncmp("zlib", value, 4)) { type = BTRFS_COMPRESS_ZLIB; + type_len = 4; } else if (!strncmp("zstd", value, 4)) { type = BTRFS_COMPRESS_ZSTD; + type_len = 4; btrfs_set_fs_incompat(fs_info, COMPRESS_ZSTD); } else { return -EINVAL; } + /* + * Parse an optional ":level" suffix. Old kernels accepted and + * silently ignored arbitrary suffixes after the algorithm name, and + * such values may still exist on disk, so fall back to the default + * level instead of returning an error when the suffix cannot be + * parsed, otherwise the inode's properties could not be loaded. + */ + if (len > type_len && value[type_len] == ':') { + char str[16]; + size_t suffix_len = len - type_len; + + if (suffix_len < sizeof(str)) { + memcpy(str, value + type_len, suffix_len); + str[suffix_len] = 0; + if (btrfs_compress_str2level(type, str, &level) < 0) + level = 0; + } + } + inode->flags &= ~BTRFS_INODE_NOCOMPRESS; inode->flags |= BTRFS_INODE_COMPRESS; inode->prop_compress = type; + inode->prop_compress_level = level; return 0; } +/* + * Regenerate the canonical "type" or "type:level" value of the + * compression property from the inode's cached fields. Used where the + * property is rewritten rather than parsed: FS_IOC_SETFLAGS preserving + * an existing property, and directory inheritance. + */ +ssize_t btrfs_prop_compression_extract(const struct btrfs_inode *inode, + char *buf, size_t size) +{ + int len; + + switch (inode->prop_compress) { + case BTRFS_COMPRESS_ZLIB: + case BTRFS_COMPRESS_LZO: + case BTRFS_COMPRESS_ZSTD: + break; + default: + return -ENODATA; + } + + if (inode->prop_compress_level) + len = snprintf(buf, size, "%s:%d", + btrfs_compress_type2str(inode->prop_compress), + inode->prop_compress_level); + else + len = snprintf(buf, size, "%s", + btrfs_compress_type2str(inode->prop_compress)); + if (len >= size) + return -EOVERFLOW; + + return len; +} + static bool prop_compression_ignore(const struct btrfs_inode *inode) { /* @@ -401,6 +462,7 @@ int btrfs_inode_inherit_props(struct btrfs_trans_handle *trans, for (i = 0; i < ARRAY_SIZE(prop_handlers); i++) { const struct prop_handler *h = &prop_handlers[i]; const char *value; + char buf[16]; u64 num_bytes = 0; if (!h->inheritable) @@ -413,6 +475,15 @@ int btrfs_inode_inherit_props(struct btrfs_trans_handle *trans, if (!value) continue; + /* + * The extract hooks return static strings, which cannot + * carry the compression property's optional ":level" + * suffix; regenerate the full value for inheritance. + */ + if (h->extract == prop_compression_extract && + btrfs_prop_compression_extract(parent, buf, sizeof(buf)) > 0) + value = buf; + /* * This is not strictly necessary as the property should be * valid, but in case it isn't, don't propagate it further. diff --git a/fs/btrfs/props.h b/fs/btrfs/props.h index 15d9a025c923d..86a004be4e469 100644 --- a/fs/btrfs/props.h +++ b/fs/btrfs/props.h @@ -24,6 +24,9 @@ bool btrfs_ignore_prop(const struct btrfs_inode *inode, const char *name); int btrfs_load_inode_props(struct btrfs_inode *inode, struct btrfs_path *path); +ssize_t btrfs_prop_compression_extract(const struct btrfs_inode *inode, + char *buf, size_t size); + int btrfs_inode_inherit_props(struct btrfs_trans_handle *trans, struct btrfs_inode *inode, const struct btrfs_inode *dir); -- 2.53.0