summaryrefslogtreecommitdiffstats
path: root/fs/ntfs/wof.c
diff options
context:
space:
mode:
authorJinke Han <jinkehan@didiglobal.com>2026-09-08 15:37:42 +0800
committerIngo Molnar <mingo@kernel.org>2026-09-17 09:48:42 +0200
commita5f7a5bb3b7f28ba7e4fa246775b29a0e5537255 (patch)
tree73fef824f9a846cf935869d0dbc5070ef6e2f741 /fs/ntfs/wof.c
downloadlinux-stable-a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255.tar.gz
linux-stable-a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255.zip
x86/kprobes: Fix crash when probing CS CALL instructionsgrafted
When using eBPF to probe CS CALL instructions within a function, a crash can be triggered. The eBPF tool probes offset 257 of the __hrtimer_run_queues() function: <__hrtimer_run_queues+249>: nopl 0x0(%rax,%rax,1) <__hrtimer_run_queues+254>: mov %r14,%rdi <__hrtimer_run_queues+257>: cs call <__x86_indirect_thunk_r12> <__hrtimer_run_queues+263>: mov %eax,%r12d <__hrtimer_run_queues+266>: xchg %ax,%ax <__hrtimer_run_queues+268>: mov %r13,%rdi Which triggers this crash: BUG: unable to handle page fault for address: 00000000000f41c9 #PF: supervisor write access in kernel mode #PF: error_code(0x0002) - not-present page PGD 0 P4D 0 Oops: 0002 [#1] SMP NOPTI CPU: 1 PID: 0 Comm: swapper/1 Kdump: loaded Tainted: P RIP: 0010:__hrtimer_run_queues+0x106/0x230 Note that __hrtimer_run_queues+0x106 is __hrtimer_run_queues+262, which is at the 6th byte of the above CS CALL instruction. Since the CS CALL instruction occupies 6 bytes, the exception occurred in the middle of that call instruction. The root cause is that when using eBPF tools to probe in the middle of a function, a kprobe with INT3 is used as the underlying implementation. During single-step emulation of the original CALL instruction, int3_emulate_call() assumes that the probed CALL instruction is 5 bytes long. However, the actual CS-prefixed CALL instruction occupies 6 bytes, so it constructs an incorrect exception return address. When the CPU returns from the kprobe handler, the next instruction to be executed is at the address of the last byte of that CS CALL instruction. Coincidentally, starting from that address, the CPU fetches and decodes a completely different instruction, which ultimately triggers a kernel crash. Fix the issue by using the actual instruction length obtained from the instruction decoder when constructing the exception return address, rather than relying on the hardcoded CALL_INSN_SIZE macro. [ mingo: Refined the changelog ] Fixes: 6256e668b7af ("x86/kprobes: Use int3 instead of debug trap for single-step") Suggested-by: Masami Hiramatsu (Google) <mhiramat@kernel.org> Signed-off-by: Jinke Han <jinkehan@didiglobal.com> Signed-off-by: Ingo Molnar <mingo@kernel.org> Reviewed-by: Masami Hiramatsu (Google) <mhiramat@kernel.org> Acked-by: Yafang Shao <laoar.shao@gmail.com> Acked-by: Borislav Petkov <bp@alien8.de> Cc: Peter Zijlstra <peterz@infradead.org> Link: https://patch.msgid.link/20260908073742.GA10517@didi-ThinkCentre-M920t-N000
Diffstat (limited to 'fs/ntfs/wof.c')
-rw-r--r--fs/ntfs/wof.c757
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;
+}