diff options
Diffstat (limited to 'fs/ntfs/wof.c')
| -rw-r--r-- | fs/ntfs/wof.c | 757 |
1 files changed, 757 insertions, 0 deletions
diff --git a/fs/ntfs/wof.c b/fs/ntfs/wof.c new file mode 100644 index 000000000..9847259e5 --- /dev/null +++ b/fs/ntfs/wof.c @@ -0,0 +1,757 @@ +// 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 *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; + } +} + +/* + * Size of the buffer a chunk is read into. A chunk is read straight off the + * device, so the buffer has to hold @comp_unit bytes plus the leading partial + * sector. + */ +static size_t ntfs_wof_input_size(const struct ntfs_wof_workspace *ws) +{ + return round_up((size_t)ws->comp_unit + 511, 512); +} + +static int ntfs_wof_workspace_prepare(struct ntfs_wof_workspace *ws) +{ + void *output, *scratch; + size_t scratch_size; + + if (ws->output) + return 0; + + scratch_size = ws->codec->scratch_size(ws->comp_unit); + if (!scratch_size) + return -EINVAL; + + output = kvmalloc(ws->comp_unit, GFP_NOFS); + scratch = kvzalloc(scratch_size, GFP_NOFS); + if (!output || !scratch) { + kvfree(output); + kvfree(scratch); + return -ENOMEM; + } + + 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->output); + kvfree(ws->scratch); + 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, u32 decomp_size, + 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 > decomp_size) + return -EINVAL; + *chunk_offset = 0; + *chunk_size = chunk_data_size; + goto out; + } + + 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)) { + 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) + return -EIO; + buf += sector_off; + } else { + struct ntfs_attr_search_ctx *ctx; + u32 value_length; + u16 value_offset; + + if (bytes_to_read > table_buf_size) + return -EINVAL; + + mutex_lock(&base_ni->mrec_lock); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); + if (!ctx) { + ret = -ENOMEM; + goto out_unlock_mrec; + } + ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); + if (ret) + goto out_put_ctx; + + value_length = + le32_to_cpu(ctx->attr->data.resident.value_length); + value_offset = + le16_to_cpu(ctx->attr->data.resident.value_offset); + if (byte_off + bytes_to_read > value_length) { + ret = -EINVAL; + goto out_put_ctx; + } + memcpy(table_buf, (u8 *)ctx->attr + value_offset + byte_off, + bytes_to_read); + buf = table_buf; +out_put_ctx: + ntfs_attr_put_search_ctx(ctx); +out_unlock_mrec: + mutex_unlock(&base_ni->mrec_lock); + if (ret) + return ret; + } + + 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] > decomp_size) + return -EINVAL; + + *chunk_offset = table_size + off[0]; + *chunk_size = off[1] - off[0]; +out: + if (!*chunk_size) + return -EINVAL; + return 0; +} + +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) +{ + struct ntfs_inode *base_ni = wof_ni->ext.base_ntfs_ino; + struct ntfs_attr_search_ctx *ctx; + u32 input_offset = chunk_offset & 511; + u32 input_size_aligned; + u32 value_length; + u16 value_offset; + int err; + + input_size_aligned = round_up(chunk_size + input_offset, 512); + if (input_size_aligned > input_size) + return -EINVAL; + + if (NInoNonResident(wof_ni)) { + 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; + } + + mutex_lock(&base_ni->mrec_lock); + ctx = ntfs_attr_get_search_ctx(base_ni, NULL); + if (!ctx) { + err = -ENOMEM; + goto out_unlock_mrec; + } + + err = ntfs_attr_lookup(wof_ni->type, wof_ni->name, wof_ni->name_len, + CASE_SENSITIVE, 0, NULL, 0, ctx); + if (err) + goto out_put_ctx; + + value_length = le32_to_cpu(ctx->attr->data.resident.value_length); + value_offset = le16_to_cpu(ctx->attr->data.resident.value_offset); + if (chunk_offset + chunk_size > value_length) { + err = -EINVAL; + goto out_put_ctx; + } + memcpy(input, (u8 *)ctx->attr + value_offset + chunk_offset, + chunk_size); + *chunk_mem = input; +out_put_ctx: + ntfs_attr_put_search_ctx(ctx); +out_unlock_mrec: + mutex_unlock(&base_ni->mrec_lock); + return err; +} + +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); +} + +/* + * Decompress one chunk into @folio. Only this step needs the workspace, so it + * is the only step that takes the workspace lock. + */ +static int ntfs_wof_decompress_chunk(struct ntfs_wof_workspace *ws, + struct ntfs_volume *vol, + struct address_space *mapping, + struct folio *folio, loff_t folio_start, + loff_t folio_end, u64 chunk_file_offset, + char *chunk_mem, u32 chunk_size, + u32 decomp_size) +{ + loff_t chunk_end = chunk_file_offset + decomp_size; + loff_t copy_start, copy_end; + int err; + + mutex_lock(ws->lock); + err = ntfs_wof_workspace_prepare(ws); + if (err) + goto out_unlock; + + err = ntfs_wof_try_direct(ws, mapping, folio, chunk_file_offset, + chunk_end, chunk_mem, chunk_size, + decomp_size); + if (err != -EAGAIN) + goto out_unlock; + + 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; + } + + 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); +out_unlock: + mutex_unlock(ws->lock); + return err; +} + +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; + void *input; + size_t input_size; + 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) && !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; + } + + input_size = ntfs_wof_input_size(ws); + input = kvmalloc(input_size, GFP_NOFS); + if (!input) { + err = -ENOMEM; + goto out_iput; + } + + 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; + + decomp_size = chunk_idx + 1 == chunk_count ? + i_size - chunk_idx * ws->comp_unit : + ws->comp_unit; + err = parse_wof_chunk_table(ni, wof_ni, chunk_idx, chunk_count, + decomp_size, &chunk_offset, + &chunk_size, input, input_size); + if (err) + goto out_free_input; + + err = ntfs_read_wof_chunk(vol, wof_ni, chunk_offset, chunk_size, + input, input_size, &chunk_mem); + if (err) + goto out_free_input; + + err = ntfs_wof_decompress_chunk(ws, vol, mapping, folio, + folio_start, folio_end, + chunk_idx * ws->comp_unit, + chunk_mem, chunk_size, + decomp_size); + if (err) + goto out_free_input; + } + + if (folio_end > i_size) + folio_zero_segment(folio, i_size - folio_start, + folio_size(folio)); +out_free_input: + kvfree(input); +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; +} |
