[PATCH v4 09/10] ntfs: add non-resident WOF decompression
Hyunchul Lee <[email protected]>
| Newsgroups | dev.linux.lists.ntfs |
|---|---|
| Message-ID | <[email protected]> |
Introduce non-resident Windows System Compression (WOF) decompression support. Add wof.c containing parse_wof_chunk_table() and ntfs_read_wof_compressed_block(), and routing them via transparent codec ops table with dynamic scratch memory allocation. Hook up ntfs_readpage/read_folio paths in aops.c to delegate to the WOF block reader when NInoWofCompressed is set. Signed-off-by: Hyunchul Lee <[email protected]> --- fs/ntfs/Makefile | 2 +- fs/ntfs/aops.c | 19 +- fs/ntfs/attrib.c | 2 +- fs/ntfs/bdev-io.c | 4 +- fs/ntfs/ea.c | 28 ++- fs/ntfs/file.c | 47 +++- fs/ntfs/inode.c | 9 +- fs/ntfs/ntfs.h | 4 + fs/ntfs/reparse.c | 59 +++-- fs/ntfs/super.c | 3 + fs/ntfs/wof.c | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 798 insertions(+), 53 deletions(-) diff --git a/fs/ntfs/Makefile b/fs/ntfs/Makefile index 5e5fd05f5863..ee8987e496a8 100644 --- a/fs/ntfs/Makefile +++ b/fs/ntfs/Makefile @@ -7,7 +7,7 @@ ntfs-y := aops.o attrib.o collate.o dir.o file.o index.o inode.o \ upcase.o bitmap.o lcnalloc.o logfile.o reparse.o compress.o \ iomap.o debug.o sysctl.o object_id.o bdev-io.o -ntfs-$(CONFIG_NTFS_FS_WOF_COMPRESSION) += \ +ntfs-$(CONFIG_NTFS_FS_WOF_COMPRESSION) += wof.o \ lib/decompress_common.o lib/lzx_decompress.o lib/xpress_decompress.o ccflags-$(CONFIG_NTFS_DEBUG) += -DDEBUG diff --git a/fs/ntfs/aops.c b/fs/ntfs/aops.c index 173de4cbee0f..517d9a1563e7 100644 --- a/fs/ntfs/aops.c +++ b/fs/ntfs/aops.c @@ -90,6 +90,14 @@ static int ntfs_read_folio(struct file *file, struct folio *folio) folio_unlock(folio); return -EOPNOTSUPP; } + if (NInoWofCompressed(ni)) { +#ifdef CONFIG_NTFS_FS_WOF_COMPRESSION + return ntfs_read_wof_compressed_block(folio); +#else + folio_unlock(folio); + return -EOPNOTSUPP; +#endif + } /* Compressed data streams are handled in compress.c. */ if (NInoNonResident(ni) && NInoCompressed(ni)) return ntfs_read_compressed_block(folio); @@ -136,11 +144,12 @@ static sector_t ntfs_bmap(struct address_space *mapping, sector_t block) ntfs_debug("Entering for mft_no 0x%llx, logical block 0x%llx.", ni->mft_no, (unsigned long long)block); if (ni->type != AT_DATA || !NInoNonResident(ni) || NInoEncrypted(ni) || - NInoMstProtected(ni)) { + NInoWofCompressed(ni) || NInoMstProtected(ni)) { ntfs_error(vol->sb, "BMAP does not make sense for %s attributes, returning 0.", (ni->type != AT_DATA) ? "non-data" : (!NInoNonResident(ni) ? "resident" : - "encrypted")); + (NInoWofCompressed(ni) ? "WOF-compressed" : + "encrypted"))); return 0; } /* None of these can happen. */ @@ -234,7 +243,8 @@ static void ntfs_readahead(struct readahead_control *rac) * Resident files are not cached in the page cache, * and readahead is not implemented for compressed files. */ - if (!NInoNonResident(ni) || NInoCompressed(ni)) + if (!NInoNonResident(ni) || NInoCompressed(ni) || + NInoWofCompressed(ni)) return; iomap_readahead(&ntfs_read_iomap_ops, &ctx, NULL); } @@ -286,6 +296,9 @@ static int ntfs_writepages(struct address_space *mapping, static int ntfs_swap_activate(struct swap_info_struct *sis, struct file *swap_file, sector_t *span) { + if (NInoWofCompressed(NTFS_I(file_inode(swap_file)))) + return -EOPNOTSUPP; + return iomap_swapfile_activate(sis, swap_file, span, &ntfs_read_iomap_ops); } diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index d9e844c481d8..60264833bb63 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -2137,7 +2137,7 @@ int ntfs_attr_make_non_resident(struct ntfs_inode *ni, const u32 data_size) ni->runlist.count = 0; write_lock_irqsave(&ni->size_lock, flags); ni->allocated_size = new_size; - if (NInoSparse(ni) || NInoCompressed(ni)) { + if ((NInoSparse(ni) && !NInoWofCompressed(ni)) || NInoCompressed(ni)) { ni->itype.compressed.size = ni->allocated_size; if (a->data.non_resident.compression_unit) { ni->itype.compressed.block_size = 1U << diff --git a/fs/ntfs/bdev-io.c b/fs/ntfs/bdev-io.c index 27d7c2767a33..86db4d9298ed 100644 --- a/fs/ntfs/bdev-io.c +++ b/fs/ntfs/bdev-io.c @@ -33,7 +33,7 @@ int ntfs_bdev_read(struct block_device *bdev, char *data, loff_t start, size_t s unsigned int done = 0, added; int error; struct bio *bio; - enum req_op op; + blk_opf_t op; sector_t sector = start >> SECTOR_SHIFT; if (start & (SECTOR_SIZE - 1)) @@ -66,7 +66,7 @@ int ntfs_bdev_read(struct block_device *bdev, char *data, loff_t start, size_t s error = submit_bio_wait(bio); bio_put(bio); - if (op == REQ_OP_READ) + if ((op & REQ_OP_MASK) == REQ_OP_READ) invalidate_kernel_vmap_range(data, size); return error; } diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index c8a3d9f07dc4..74995a84c88c 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -838,6 +838,17 @@ static bool ntfs_is_reserved_lxattr(const char *name) !strcmp(name, "$LXMOD") || !strcmp(name, "$LXDEV"); } +static int ntfs_validate_fattr(struct ntfs_inode *ni, __le32 fattr) +{ + const __le32 wof_flags = FILE_ATTR_SPARSE_FILE | + FILE_ATTR_REPARSE_POINT; + + if (NInoWofCompressed(ni) && ((ni->flags ^ fattr) & wof_flags)) + return -EPERM; + + return 0; +} + static int ntfs_setxattr(const struct xattr_handler *handler, struct mnt_idmap *idmap, struct dentry *unused, struct inode *inode, const char *name, const void *value, @@ -858,7 +869,8 @@ static int ntfs_setxattr(const struct xattr_handler *handler, err = -EINVAL; goto out; } - fattr = cpu_to_le32(*(u8 *)value); + fattr = cpu_to_le32((le32_to_cpu(ni->flags) & ~0xffU) | + *(u8 *)value); goto set_fattr; } @@ -873,6 +885,10 @@ static int ntfs_setxattr(const struct xattr_handler *handler, else fattr = cpu_to_le32(*(u32 *)value); + err = ntfs_validate_fattr(ni, fattr); + if (err) + goto out; + if (S_ISREG(inode->i_mode)) { mutex_lock(&ni->mrec_lock); err = ntfs_new_attr_flags(ni, fattr); @@ -887,6 +903,10 @@ static int ntfs_setxattr(const struct xattr_handler *handler, else fattr &= ~FILE_ATTR_DIRECTORY; + err = ntfs_validate_fattr(ni, fattr); + if (err) + goto out; + if (ni->flags != fattr) { ni->flags = fattr; if (fattr & FILE_ATTR_READONLY) @@ -905,8 +925,10 @@ static int ntfs_setxattr(const struct xattr_handler *handler, mutex_unlock(&ni->mrec_lock); out: - inode_set_ctime_current(inode); - mark_inode_dirty(inode); + if (!err) { + inode_set_ctime_current(inode); + mark_inode_dirty(inode); + } return err; } diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index d4282822b3ce..88747217ba61 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -128,7 +128,8 @@ static int ntfs_trim_prealloc(struct inode *vi) static int ntfs_file_release(struct inode *vi, struct file *filp) { - if (!NInoCompressed(NTFS_I(vi))) + if (!NInoCompressed(NTFS_I(vi)) && + !NInoWofCompressed(NTFS_I(vi))) return ntfs_trim_prealloc(vi); return 0; @@ -256,10 +257,11 @@ static int ntfs_setattr_size(struct inode *vi, struct iattr *attr) int err; loff_t old_size = vi->i_size; - if (NInoCompressed(ni) || NInoEncrypted(ni)) { - ntfs_warning(vi->i_sb, - "Changes in inode size are not supported yet for %s files, ignoring.", - NInoCompressed(ni) ? "compressed" : "encrypted"); + if (NInoCompressed(ni) || NInoEncrypted(ni) || NInoWofCompressed(ni)) { + ntfs_warning( + vi->i_sb, + "Changes in inode size are not supported yet for %s files.", + NInoEncrypted(ni) ? "encrypted" : "compressed"); return -EOPNOTSUPP; } @@ -308,6 +310,13 @@ int ntfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, if (err) goto out; + if ((ia_valid & ATTR_SIZE) && + (NInoCompressed(ni) || NInoEncrypted(ni) || + NInoWofCompressed(ni))) { + err = -EOPNOTSUPP; + goto out; + } + if (!(vol->vol_flags & VOLUME_IS_DIRTY)) ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); @@ -370,7 +379,7 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path, stat->result_mask |= STATX_BTIME; stat->btime = NTFS_I(inode)->i_crtime; - if (NInoCompressed(ni)) + if (NInoCompressed(ni) || NInoWofCompressed(ni)) stat->attributes |= STATX_ATTR_COMPRESSED; if (NInoEncrypted(ni)) @@ -395,7 +404,8 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path, bdev_logical_block_size(inode->i_sb->s_bdev); stat->result_mask |= STATX_DIOALIGN; - if (!NInoCompressed(ni) && !NInoEncrypted(ni)) { + if (!NInoCompressed(ni) && !NInoEncrypted(ni) && + !NInoWofCompressed(ni)) { stat->dio_mem_align = align; stat->dio_offset_align = align; } @@ -408,6 +418,10 @@ static loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence) { struct inode *inode = file->f_mapping->host; + if (NInoWofCompressed(NTFS_I(inode)) && + (whence == SEEK_HOLE || whence == SEEK_DATA)) + return -EOPNOTSUPP; + switch (whence) { case SEEK_HOLE: inode_lock_shared(inode); @@ -438,7 +452,8 @@ static ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to) if (NVolShutdown(NTFS_SB(sb))) return -EIO; - if (NInoCompressed(NTFS_I(vi)) && iocb->ki_flags & IOCB_DIRECT) + if ((NInoCompressed(NTFS_I(vi)) || NInoWofCompressed(NTFS_I(vi))) && + iocb->ki_flags & IOCB_DIRECT) return -EOPNOTSUPP; inode_lock_shared(vi); @@ -569,6 +584,9 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) if (NVolShutdown(vol)) return -EIO; + if (NInoWofCompressed(ni)) + return -EOPNOTSUPP; + if (NInoEncrypted(ni)) { ntfs_error(vi->i_sb, "Writing for %s files is not supported yet", NInoCompressed(ni) ? "Compressed" : "Encrypted"); @@ -653,6 +671,9 @@ static vm_fault_t ntfs_filemap_page_mkwrite(struct vm_fault *vmf) struct inode *inode = file_inode(vmf->vma->vm_file); vm_fault_t ret; + if (NInoWofCompressed(NTFS_I(inode))) + return VM_FAULT_SIGBUS; + sb_start_pagefault(inode->i_sb); file_update_time(vmf->vma->vm_file); @@ -675,7 +696,7 @@ static int ntfs_file_mmap_prepare(struct vm_area_desc *desc) if (NVolShutdown(NTFS_SB(file->f_mapping->host->i_sb))) return -EIO; - if (NInoCompressed(NTFS_I(inode))) + if (NInoCompressed(NTFS_I(inode)) || NInoWofCompressed(NTFS_I(inode))) return -EOPNOTSUPP; if (vma_desc_test_all(desc, VMA_SHARED_BIT, VMA_MAYWRITE_BIT)) { @@ -703,6 +724,9 @@ static int ntfs_file_mmap_prepare(struct vm_area_desc *desc) static int ntfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo, u64 start, u64 len) { + if (NInoWofCompressed(NTFS_I(inode))) + return -EOPNOTSUPP; + return iomap_fiemap(inode, fieinfo, start, len, &ntfs_read_iomap_ops); } @@ -1097,6 +1121,9 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le if (mode & ~(NTFS_FALLOC_FL_SUPPORTED)) return -EOPNOTSUPP; + if (NInoCompressed(ni) || NInoEncrypted(ni) || NInoWofCompressed(ni)) + return -EOPNOTSUPP; + if (!NVolFreeClusterKnown(vol)) wait_event(vol->free_waitq, NVolFreeClusterKnown(vol)); @@ -1120,7 +1147,7 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le old_size = i_size_read(vi); inode_lock(vi); - if (NInoCompressed(ni) || NInoEncrypted(ni)) { + if (NInoCompressed(ni) || NInoEncrypted(ni) || NInoWofCompressed(ni)) { err = -EOPNOTSUPP; goto out; } diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index ea97a6d028a3..32edb4045178 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -1092,6 +1092,11 @@ static int ntfs_read_locked_inode(struct inode *vi) /* Setup the state. */ if (a->flags & (ATTR_COMPRESSION_MASK | ATTR_IS_SPARSE)) { if (a->flags & ATTR_COMPRESSION_MASK) { + if (NInoWofCompressed(ni)) { + ntfs_error(vi->i_sb, + "Found native compression on a WOF file."); + goto unm_err_out; + } NInoSetCompressed(ni); ni->flags |= FILE_ATTR_COMPRESSED; if (vol->cluster_size > 4096) { @@ -1122,7 +1127,7 @@ static int ntfs_read_locked_inode(struct inode *vi) } if (a->non_resident) { NInoSetNonResident(ni); - if (NInoCompressed(ni) || NInoSparse(ni)) { + if (NInoCompressed(ni) || (NInoSparse(ni) && !NInoWofCompressed(ni))) { if (NInoCompressed(ni) && a->data.non_resident.compression_unit != 4) { ntfs_error(vi->i_sb, @@ -1401,7 +1406,7 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) "Attribute name is placed after the mapping pairs array."); goto unm_err_out; } - if (NInoCompressed(ni) || NInoSparse(ni)) { + if (NInoCompressed(ni) || (NInoSparse(ni) && !NInoWofCompressed(ni))) { if (NInoCompressed(ni) && a->data.non_resident.compression_unit != 4) { ntfs_error(vi->i_sb, "Found non-standard compression unit (%u instead of 4). Cannot handle this.", diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index 45064dbcc2e4..df5a75d506f6 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -202,6 +202,10 @@ static inline struct ntfs_volume *NTFS_SB(struct super_block *sb) /* From fs/ntfs/compress.c */ int ntfs_read_compressed_block(struct folio *folio); +#ifdef CONFIG_NTFS_FS_WOF_COMPRESSION +int ntfs_read_wof_compressed_block(struct folio *folio); +void ntfs_wof_free_workspaces(void); +#endif int allocate_compression_buffers(void); void free_compression_buffers(void); int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c index 0e3ed9f520dc..5e483a2f9060 100644 --- a/fs/ntfs/reparse.c +++ b/fs/ntfs/reparse.c @@ -223,25 +223,9 @@ static bool valid_reparse_data(struct ntfs_inode *ni, return false; break; case IO_REPARSE_TAG_WOF: { - const struct wof_reparse_data *wof_data = - (const struct wof_reparse_data *)reparse_attr->reparse_data; - if (!valid_reparse_buffer(ni, reparse_attr, size, sizeof(struct wof_reparse_data))) return false; - - if (le16_to_cpu(reparse_attr->reparse_data_length) < - sizeof(struct wof_reparse_data) || - wof_data->version != WOF_CURRENT_VERSION || - wof_data->provider != WOF_PROVIDER_FILE || - wof_data->provider_version != - WOF_PROVIDER_CURRENT_VERSION) - return false; - if (wof_data->compression_format != WOF_COMPRESSION_XPRESS4K && - wof_data->compression_format != WOF_COMPRESSION_XPRESS8K && - wof_data->compression_format != WOF_COMPRESSION_XPRESS16K && - wof_data->compression_format != WOF_COMPRESSION_LZX) - return false; break; } default: @@ -361,22 +345,35 @@ int ntfs_parse_reparse(struct ntfs_inode *ni, unsigned int *mode) const struct wof_reparse_data *wof_data = (const struct wof_reparse_data *)reparse_attr->reparse_data; - switch (wof_data->compression_format) { - case WOF_COMPRESSION_XPRESS4K: - ni->itype.compressed.block_size_bits = 12; - break; - case WOF_COMPRESSION_XPRESS8K: - ni->itype.compressed.block_size_bits = 13; - break; - case WOF_COMPRESSION_XPRESS16K: - ni->itype.compressed.block_size_bits = 14; - break; - case WOF_COMPRESSION_LZX: - ni->itype.compressed.block_size_bits = 15; - break; + ni->itype.compressed.block_size_bits = 0; + ni->itype.compressed.block_size = 0; + if (wof_data->version == WOF_CURRENT_VERSION && + wof_data->provider == WOF_PROVIDER_FILE && + wof_data->provider_version == + WOF_PROVIDER_CURRENT_VERSION) { + switch (wof_data->compression_format) { + case WOF_COMPRESSION_XPRESS4K: + ni->itype.compressed.block_size_bits = + 12; + break; + case WOF_COMPRESSION_XPRESS8K: + ni->itype.compressed.block_size_bits = + 13; + break; + case WOF_COMPRESSION_XPRESS16K: + ni->itype.compressed.block_size_bits = + 14; + break; + case WOF_COMPRESSION_LZX: + ni->itype.compressed.block_size_bits = + 15; + break; + } } - ni->itype.compressed.block_size = - 1 << ni->itype.compressed.block_size_bits; + if (ni->itype.compressed.block_size_bits) + ni->itype.compressed.block_size = + 1 + << ni->itype.compressed.block_size_bits; #endif NInoSetWofCompressed(ni); VFS_I(ni)->i_mode &= ~0222; diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 8abe7bee4c0d..ab3f9847eea3 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -2691,6 +2691,9 @@ static void __exit exit_ntfs_fs(void) * destroy cache. */ rcu_barrier(); +#ifdef CONFIG_NTFS_FS_WOF_COMPRESSION + ntfs_wof_free_workspaces(); +#endif kmem_cache_destroy(ntfs_big_inode_cache); kmem_cache_destroy(ntfs_inode_cache); kmem_cache_destroy(ntfs_name_cache); diff --git a/fs/ntfs/wof.c b/fs/ntfs/wof.c new file mode 100644 index 000000000000..407c180a733d --- /dev/null +++ b/fs/ntfs/wof.c @@ -0,0 +1,674 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Windows System Compression (WOF) decompression glue. + * + * Copyright (c) 2026 LG Electronics Co., Ltd. + */ + +#include <linux/fs.h> +#include <linux/blkdev.h> +#include <linux/overflow.h> +#include <linux/pagemap.h> +#include <linux/sched/mm.h> +#include <linux/slab.h> +#include <linux/unaligned.h> +#include <linux/vmalloc.h> + +#include "ntfs.h" +#include "inode.h" +#include "debug.h" +#include "ntfs_codec.h" +#include "attrib.h" + +static const __le16 WOF_NAME[] = { + cpu_to_le16('W'), cpu_to_le16('o'), cpu_to_le16('f'), + cpu_to_le16('C'), cpu_to_le16('o'), cpu_to_le16('m'), + cpu_to_le16('p'), cpu_to_le16('r'), cpu_to_le16('e'), + cpu_to_le16('s'), cpu_to_le16('s'), cpu_to_le16('e'), + cpu_to_le16('d'), cpu_to_le16('D'), cpu_to_le16('a'), + cpu_to_le16('t'), cpu_to_le16('a'), +}; + +#define WOF_NAME_LEN 17 + +#define NTFS_WOF_MAX_COMP_UNIT (1U << 15) +#define NTFS_WOF_MAX_PAGES \ + DIV_ROUND_UP(NTFS_WOF_MAX_COMP_UNIT + PAGE_SIZE - 1, PAGE_SIZE) + +struct ntfs_wof_workspace { + struct mutex *lock; + const struct ntfs_codec_ops *codec; + u32 comp_unit; + void *input; + size_t input_size; + void *output; + void *scratch; +}; + +static DEFINE_MUTEX(ntfs_wof_xpress4k_lock); +static DEFINE_MUTEX(ntfs_wof_xpress8k_lock); +static DEFINE_MUTEX(ntfs_wof_xpress16k_lock); +static DEFINE_MUTEX(ntfs_wof_lzx32k_lock); + +static struct ntfs_wof_workspace ntfs_wof_xpress4k_workspace = { + .lock = &ntfs_wof_xpress4k_lock, + .codec = &ntfs_xpress4k_codec_ops, + .comp_unit = 1U << 12, +}; + +static struct ntfs_wof_workspace ntfs_wof_xpress8k_workspace = { + .lock = &ntfs_wof_xpress8k_lock, + .codec = &ntfs_xpress8k_codec_ops, + .comp_unit = 1U << 13, +}; + +static struct ntfs_wof_workspace ntfs_wof_xpress16k_workspace = { + .lock = &ntfs_wof_xpress16k_lock, + .codec = &ntfs_xpress16k_codec_ops, + .comp_unit = 1U << 14, +}; + +static struct ntfs_wof_workspace ntfs_wof_lzx32k_workspace = { + .lock = &ntfs_wof_lzx32k_lock, + .codec = &ntfs_lzx32k_codec_ops, + .comp_unit = 1U << 15, +}; + +static struct ntfs_wof_workspace *const ntfs_wof_workspaces[] = { + &ntfs_wof_xpress4k_workspace, + &ntfs_wof_xpress8k_workspace, + &ntfs_wof_xpress16k_workspace, + &ntfs_wof_lzx32k_workspace, +}; + +static struct ntfs_wof_workspace *ntfs_wof_workspace(u8 block_size_bits) +{ + switch (block_size_bits) { + case 12: + return &ntfs_wof_xpress4k_workspace; + case 13: + return &ntfs_wof_xpress8k_workspace; + case 14: + return &ntfs_wof_xpress16k_workspace; + case 15: + return &ntfs_wof_lzx32k_workspace; + default: + return NULL; + } +} + +static int ntfs_wof_workspace_prepare(struct ntfs_wof_workspace *ws) +{ + void *input, *output, *scratch; + size_t scratch_size; + + if (ws->input) + return 0; + + ws->input_size = round_up((size_t)ws->comp_unit + 511, 512); + scratch_size = ws->codec->scratch_size(ws->comp_unit); + if (!scratch_size) + return -EINVAL; + + input = kvmalloc(ws->input_size, GFP_NOFS); + output = kvmalloc(ws->comp_unit, GFP_NOFS); + scratch = kvzalloc(scratch_size, GFP_NOFS); + if (!input || !output || !scratch) { + kvfree(input); + kvfree(output); + kvfree(scratch); + return -ENOMEM; + } + + ws->input = input; + ws->output = output; + ws->scratch = scratch; + return 0; +} + +void ntfs_wof_free_workspaces(void) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(ntfs_wof_workspaces); i++) { + struct ntfs_wof_workspace *ws = ntfs_wof_workspaces[i]; + + mutex_lock(ws->lock); + kvfree(ws->input); + kvfree(ws->output); + kvfree(ws->scratch); + ws->input = NULL; + ws->output = NULL; + ws->scratch = NULL; + mutex_unlock(ws->lock); + } +} + +static int ntfs_bdev_read_from_rl(struct ntfs_volume *vol, + struct runlist *runlist, + sector_t start_sector, u64 sector_count, + void *buf) +{ + struct runlist_element *rl; + u32 sec_per_clu_bits; + s64 vcn; + u64 sec_off; + size_t buf_off = 0; + unsigned int nofs_flags; + int err; + + if (vol->cluster_size_bits < 9) + return -EINVAL; + sec_per_clu_bits = vol->cluster_size_bits - 9; + vcn = start_sector >> sec_per_clu_bits; + sec_off = start_sector & ((1ULL << sec_per_clu_bits) - 1); + + nofs_flags = memalloc_nofs_save(); + down_read(&runlist->lock); + if (!runlist->rl) { + err = -EINVAL; + goto out_unlock; + } + + rl = __ntfs_attr_find_vcn_nolock(runlist, vcn); + if (IS_ERR(rl)) { + err = PTR_ERR(rl); + goto out_unlock; + } + + while (sector_count > 0) { + s64 lcn; + s64 rl_end; + u64 byte_off, byte_len, sectors, available; + + if (rl->length <= 0 || vcn < rl->vcn) { + err = -EINVAL; + goto out_unlock; + } + + lcn = ntfs_rl_vcn_to_lcn(rl, vcn); + if (lcn < 0 && lcn != LCN_HOLE) { + err = -EINVAL; + goto out_unlock; + } + + if (check_add_overflow(rl->vcn, rl->length, &rl_end) || + rl_end <= vcn || + (u64)(rl_end - vcn) > (U64_MAX >> sec_per_clu_bits)) { + err = -EOVERFLOW; + goto out_unlock; + } + available = (u64)(rl_end - vcn) << sec_per_clu_bits; + if (available <= sec_off) { + err = -EINVAL; + goto out_unlock; + } + available -= sec_off; + sectors = min_t(u64, sector_count, available); + if (check_mul_overflow(sectors, (u64)SECTOR_SIZE, &byte_len) || + byte_len > SIZE_MAX - buf_off) { + err = -EOVERFLOW; + goto out_unlock; + } + + if (lcn == LCN_HOLE) { + memset((u8 *)buf + buf_off, 0, byte_len); + } else { + byte_off = ntfs_cluster_to_bytes(vol, lcn); + if (check_add_overflow(byte_off, sec_off << 9, + &byte_off) || + byte_off > S64_MAX) { + err = -EOVERFLOW; + goto out_unlock; + } + err = ntfs_bdev_read(vol->sb->s_bdev, + (char *)buf + buf_off, + (loff_t)byte_off, byte_len); + if (err) + goto out_unlock; + } + + buf_off += byte_len; + sector_count -= sectors; + rl++; + vcn = rl->vcn; + sec_off = 0; + } + + err = 0; +out_unlock: + up_read(&runlist->lock); + memalloc_nofs_restore(nofs_flags); + return err; +} + +static int parse_wof_chunk_table(struct ntfs_inode *base_ni, + struct ntfs_inode *ni, u64 chunk_idx, + u64 chunk_count, u64 *chunk_offset, + u32 *chunk_size, void *table_buf, + size_t table_buf_size) +{ + u8 bytes_per_off; + u8 *buf; + u64 off[2]; + u64 byte_off, chunk_data_size, table_size; + u32 bytes_to_read; + int ret = 0; + + if (i_size_read(VFS_I(base_ni)) < (1ULL << 32)) + bytes_per_off = sizeof(__le32); + else + bytes_per_off = sizeof(__le64); + + if (!chunk_count || chunk_idx >= chunk_count) + return -EINVAL; + + table_size = (chunk_count - 1) * bytes_per_off; + if (ni->data_size < 0 || (u64)ni->data_size < table_size) + return -EINVAL; + chunk_data_size = (u64)ni->data_size - table_size; + + if (chunk_count == 1) { + if (chunk_data_size > U32_MAX) + return -EINVAL; + *chunk_offset = 0; + *chunk_size = chunk_data_size; + return 0; + } + + byte_off = chunk_idx ? (chunk_idx - 1) * bytes_per_off : 0; + bytes_to_read = chunk_idx + 1 == chunk_count ? + bytes_per_off : + (chunk_idx ? 2 : 1) * bytes_per_off; + + if (!NInoNonResident(ni)) + return -EOPNOTSUPP; + + { + sector_t start_sector = byte_off >> 9; + u32 sector_off = byte_off & ((1 << 9) - 1); + u32 sectors = DIV_ROUND_UP(sector_off + bytes_to_read, 512); + + if ((size_t)sectors << 9 > table_buf_size) + return -EINVAL; + buf = table_buf; + ret = ntfs_bdev_read_from_rl(ni->vol, &ni->runlist, + start_sector, sectors, buf); + if (ret) { + ret = -EIO; + return ret; + } + buf += sector_off; + } + + if (bytes_per_off == sizeof(__le32)) { + off[0] = chunk_idx ? get_unaligned_le32(buf) : 0; + if (chunk_idx + 1 == chunk_count) + off[1] = chunk_data_size; + else if (chunk_idx) + off[1] = get_unaligned_le32(buf + bytes_per_off); + else + off[1] = get_unaligned_le32(buf); + } else { + off[0] = chunk_idx ? get_unaligned_le64(buf) : 0; + if (chunk_idx + 1 == chunk_count) + off[1] = chunk_data_size; + else if (chunk_idx) + off[1] = get_unaligned_le64(buf + bytes_per_off); + else + off[1] = get_unaligned_le64(buf); + } + + if (off[1] <= off[0] || off[1] > chunk_data_size || + off[1] - off[0] > U32_MAX) { + ret = -EINVAL; + return ret; + } + + *chunk_offset = table_size + off[0]; + *chunk_size = off[1] - off[0]; + return ret; +} + +static int ntfs_read_wof_chunk(struct ntfs_volume *vol, + struct ntfs_inode *wof_ni, u64 chunk_offset, + u32 chunk_size, void *input, size_t input_size, + char **chunk_mem) +{ + u32 input_offset = chunk_offset & 511; + u32 input_size_aligned; + int err; + + input_size_aligned = round_up(chunk_size + input_offset, 512); + if (input_size_aligned > input_size) + return -EINVAL; + + if (!NInoNonResident(wof_ni)) + return -EOPNOTSUPP; + + err = ntfs_bdev_read_from_rl(vol, &wof_ni->runlist, chunk_offset >> 9, + input_size_aligned >> 9, input); + if (err) + return err; + *chunk_mem = (u8 *)input + input_offset; + return 0; +} + +struct ntfs_wof_dest { + struct folio *folios[NTFS_WOF_MAX_PAGES]; + struct page *pages[NTFS_WOF_MAX_PAGES]; + unsigned int nr_folios; + unsigned int nr_pages; +}; + +static void ntfs_wof_release_dest(struct ntfs_wof_dest *dest, + struct folio *target, bool success) +{ + unsigned int i; + + for (i = 0; i < dest->nr_folios; i++) { + struct folio *folio = dest->folios[i]; + + if (folio == target) + continue; + if (success) { + flush_dcache_folio(folio); + folio_mark_uptodate(folio); + } else { + folio_clear_uptodate(folio); + } + folio_unlock(folio); + folio_put(folio); + } +} + +static int ntfs_wof_collect_dest(struct address_space *mapping, + struct folio *target, loff_t chunk_start, + loff_t chunk_end, struct ntfs_wof_dest *dest) +{ + pgoff_t index, last, page_index; + unsigned int i; + + memset(dest, 0, sizeof(*dest)); + index = chunk_start >> PAGE_SHIFT; + last = (chunk_end - 1) >> PAGE_SHIFT; + while (index <= last) { + struct folio *folio; + pgoff_t next; + bool is_target; + + if (folio_contains(target, index)) { + folio = target; + is_target = true; + } else { + folio = __filemap_get_folio( + mapping, index, + FGP_LOCK | FGP_CREAT | FGP_NOFS | FGP_NOWAIT, + GFP_NOFS); + if (IS_ERR(folio)) + return PTR_ERR(folio); + is_target = false; + if (folio_pos(folio) < chunk_start || + folio_next_pos(folio) > chunk_end) { + folio_unlock(folio); + folio_put(folio); + return -EAGAIN; + } + } + + if (dest->nr_folios == ARRAY_SIZE(dest->folios)) { + if (!is_target) { + folio_unlock(folio); + folio_put(folio); + } + return -EINVAL; + } + dest->folios[dest->nr_folios++] = folio; + next = folio->index + folio_nr_pages(folio); + if (next <= index) + return -EAGAIN; + index = next; + } + + for (page_index = chunk_start >> PAGE_SHIFT; page_index <= last; + page_index++) { + struct folio *folio = NULL; + + for (i = 0; i < dest->nr_folios; i++) { + if (folio_contains(dest->folios[i], page_index)) { + folio = dest->folios[i]; + break; + } + } + if (!folio || dest->nr_pages == ARRAY_SIZE(dest->pages)) + return -EAGAIN; + dest->pages[dest->nr_pages++] = + folio_page(folio, page_index - folio->index); + } + return 0; +} + +static int ntfs_wof_decode(struct ntfs_wof_workspace *ws, const void *src, + u32 src_len, void *dst, u32 dst_len) +{ + if (src_len == dst_len) { + memcpy(dst, src, dst_len); + return 0; + } + return ws->codec->decompress_chunk(ws->scratch, src, src_len, dst, + dst_len, ws->comp_unit); +} + +static int ntfs_wof_decode_page_direct(struct ntfs_wof_workspace *ws, + struct folio *target, loff_t chunk_start, + const void *src, u32 src_len, + u32 dst_len) +{ + unsigned int page_offset = offset_in_page(chunk_start); + struct page *page; + pgoff_t page_index; + void *addr; + int err; + + page_index = chunk_start >> PAGE_SHIFT; + if (!folio_contains(target, page_index)) + return -EAGAIN; + + page = folio_page(target, page_index - target->index); + addr = kmap_local_page(page); + err = ntfs_wof_decode(ws, src, src_len, (u8 *)addr + page_offset, + dst_len); + kunmap_local(addr); + if (err) + return -EINVAL; + return 0; +} + +static int ntfs_wof_decode_folios_direct(struct ntfs_wof_workspace *ws, + struct address_space *mapping, + struct folio *target, + loff_t chunk_start, loff_t chunk_end, + const void *src, u32 src_len, + u32 dst_len) +{ + unsigned int page_offset = offset_in_page(chunk_start); + struct ntfs_wof_dest dest; + void *addr; + unsigned int nofs_flags; + int err; + + err = ntfs_wof_collect_dest(mapping, target, chunk_start, chunk_end, + &dest); + if (err) { + ntfs_wof_release_dest(&dest, target, false); + return -EAGAIN; + } + + nofs_flags = memalloc_nofs_save(); + addr = vmap(dest.pages, dest.nr_pages, VM_MAP, PAGE_KERNEL); + memalloc_nofs_restore(nofs_flags); + if (!addr) { + ntfs_wof_release_dest(&dest, target, false); + return -EAGAIN; + } + + err = ntfs_wof_decode(ws, src, src_len, (u8 *)addr + page_offset, + dst_len); + vunmap(addr); + if (err) { + ntfs_wof_release_dest(&dest, target, false); + return -EINVAL; + } + ntfs_wof_release_dest(&dest, target, true); + return 0; +} + +static int ntfs_wof_try_direct(struct ntfs_wof_workspace *ws, + struct address_space *mapping, + struct folio *target, loff_t chunk_start, + loff_t chunk_end, const void *src, u32 src_len, + u32 dst_len) +{ + unsigned int page_offset = offset_in_page(chunk_start); + + if (dst_len <= PAGE_SIZE - page_offset) + return ntfs_wof_decode_page_direct(ws, target, chunk_start, src, + src_len, dst_len); + + return ntfs_wof_decode_folios_direct(ws, mapping, target, chunk_start, + chunk_end, src, src_len, dst_len); +} + +int ntfs_read_wof_compressed_block(struct folio *folio) +{ + struct address_space *mapping = folio->mapping; + struct ntfs_inode *ni = NTFS_I(mapping->host), *wof_ni; + struct inode *wof_inode; + struct ntfs_volume *vol = ni->vol; + struct ntfs_wof_workspace *ws; + loff_t i_size = i_size_read(VFS_I(ni)); + loff_t folio_start = folio_pos(folio); + loff_t folio_end = folio_next_pos(folio); + char *chunk_mem; + u32 decomp_size; + u64 chunk_count, chunk_idx, last_chunk, chunk_offset; + int err = 0; + + ws = ntfs_wof_workspace(ni->itype.compressed.block_size_bits); + if (!ws) { + err = -EOPNOTSUPP; + goto out; + } + + if (folio_start >= i_size) { + folio_zero_segment(folio, 0, folio_size(folio)); + goto out; + } + + wof_inode = ntfs_attr_iget(VFS_I(ni), AT_DATA, (__le16 *)WOF_NAME, + WOF_NAME_LEN); + if (IS_ERR(wof_inode)) { + err = PTR_ERR(wof_inode); + goto out; + } + + wof_ni = NTFS_I(wof_inode); + if (wof_ni->initialized_size != wof_ni->data_size) { + ntfs_error(vol->sb, + "WOF compressed stream is not fully initialized (init %lld, data %lld).", + wof_ni->initialized_size, wof_ni->data_size); + err = -EIO; + goto out_iput; + } + if (!NInoNonResident(wof_ni)) { + err = -EOPNOTSUPP; + goto out_iput; + } + if (!NInoFullyMapped(wof_ni)) { + down_write(&wof_ni->runlist.lock); + if (!NInoFullyMapped(wof_ni)) + err = ntfs_attr_map_whole_runlist(wof_ni); + up_write(&wof_ni->runlist.lock); + if (err) + goto out_iput; + } + + mutex_lock(ws->lock); + err = ntfs_wof_workspace_prepare(ws); + if (err) + goto out_unlock_ws; + + chunk_idx = div_u64(folio_start, ws->comp_unit); + last_chunk = + div_u64(min_t(loff_t, folio_end, i_size) - 1, ws->comp_unit); + chunk_count = DIV_ROUND_UP_ULL(i_size, ws->comp_unit); + for (; chunk_idx <= last_chunk; chunk_idx++) { + u32 chunk_size; + u64 chunk_file_offset; + loff_t chunk_end, copy_start, copy_end; + + err = parse_wof_chunk_table(ni, wof_ni, chunk_idx, chunk_count, + &chunk_offset, &chunk_size, + ws->input, ws->input_size); + if (err) + goto out_unlock_ws; + decomp_size = chunk_idx + 1 == chunk_count ? + i_size - chunk_idx * ws->comp_unit : + ws->comp_unit; + if (!chunk_size || chunk_size > decomp_size) { + ntfs_error( + vol->sb, + "Invalid compressed size (%u) for frame size (%u)", + chunk_size, decomp_size); + err = -EINVAL; + goto out_unlock_ws; + } + + err = ntfs_read_wof_chunk(vol, wof_ni, chunk_offset, chunk_size, + ws->input, ws->input_size, + &chunk_mem); + if (err) + goto out_unlock_ws; + + chunk_file_offset = chunk_idx * ws->comp_unit; + chunk_end = chunk_file_offset + decomp_size; + err = ntfs_wof_try_direct(ws, mapping, folio, chunk_file_offset, + chunk_end, chunk_mem, chunk_size, + decomp_size); + if (!err) + continue; + if (err != -EAGAIN) + goto out_unlock_ws; + + err = ntfs_wof_decode(ws, chunk_mem, chunk_size, ws->output, + decomp_size); + if (err) { + ntfs_error(vol->sb, "Decompression failed: %d", err); + err = -EINVAL; + goto out_unlock_ws; + } + copy_start = max_t(loff_t, folio_start, chunk_file_offset); + copy_end = min_t(loff_t, folio_end, + chunk_file_offset + decomp_size); + memcpy_to_folio(folio, copy_start - folio_start, + ws->output + copy_start - chunk_file_offset, + copy_end - copy_start); + } + + if (folio_end > i_size) + folio_zero_segment(folio, i_size - folio_start, + folio_size(folio)); +out_unlock_ws: + mutex_unlock(ws->lock); +out_iput: + iput(wof_inode); +out: + if (!err) { + flush_dcache_folio(folio); + folio_mark_uptodate(folio); + } else { + folio_clear_uptodate(folio); + } + folio_unlock(folio); + return err; +} -- 2.43.0