linux-stable/fs/xfs/scrub/stats.c
Darrick J. Wong e031928200 xfs: only call xchk_stats_merge after validating scrub inputs
Harshit Mogalapalli slogged through several reports from our internal
syzbot instance and observed that they all had a common stack trace:

BUG: KASAN: user-memory-access in instrument_atomic_read_write include/linux/instrumented.h:96 [inline]
BUG: KASAN: user-memory-access in atomic_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:1294 [inline]
BUG: KASAN: user-memory-access in queued_spin_lock include/asm-generic/qspinlock.h:111 [inline]
BUG: KASAN: user-memory-access in do_raw_spin_lock include/linux/spinlock.h:187 [inline]
BUG: KASAN: user-memory-access in __raw_spin_lock include/linux/spinlock_api_smp.h:134 [inline]
BUG: KASAN: user-memory-access in _raw_spin_lock+0x76/0xe0 kernel/locking/spinlock.c:154
Write of size 4 at addr 0000001dd87ee280 by task syz-executor365/1543

CPU: 2 PID: 1543 Comm: syz-executor365 Not tainted 6.5.0-syzk #1
Hardware name: Red Hat KVM, BIOS 1.13.0-2.module+el8.3.0+7860+a7792d29 04/01/2014
Call Trace:
 <TASK>
 __dump_stack lib/dump_stack.c:88 [inline]
 dump_stack_lvl+0x83/0xb0 lib/dump_stack.c:106
 print_report+0x3f8/0x620 mm/kasan/report.c:478
 kasan_report+0xb0/0xe0 mm/kasan/report.c:588
 check_region_inline mm/kasan/generic.c:181 [inline]
 kasan_check_range+0x139/0x1e0 mm/kasan/generic.c:187
 instrument_atomic_read_write include/linux/instrumented.h:96 [inline]
 atomic_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:1294 [inline]
 queued_spin_lock include/asm-generic/qspinlock.h:111 [inline]
 do_raw_spin_lock include/linux/spinlock.h:187 [inline]
 __raw_spin_lock include/linux/spinlock_api_smp.h:134 [inline]
 _raw_spin_lock+0x76/0xe0 kernel/locking/spinlock.c:154
 spin_lock include/linux/spinlock.h:351 [inline]
 xchk_stats_merge_one.isra.1+0x39/0x650 fs/xfs/scrub/stats.c:191
 xchk_stats_merge+0x5f/0xe0 fs/xfs/scrub/stats.c:225
 xfs_scrub_metadata+0x252/0x14e0 fs/xfs/scrub/scrub.c:599
 xfs_ioc_scrub_metadata+0xc8/0x160 fs/xfs/xfs_ioctl.c:1646
 xfs_file_ioctl+0x3fd/0x1870 fs/xfs/xfs_ioctl.c:1955
 vfs_ioctl fs/ioctl.c:51 [inline]
 __do_sys_ioctl fs/ioctl.c:871 [inline]
 __se_sys_ioctl fs/ioctl.c:857 [inline]
 __x64_sys_ioctl+0x199/0x220 fs/ioctl.c:857
 do_syscall_x64 arch/x86/entry/common.c:50 [inline]
 do_syscall_64+0x3e/0x90 arch/x86/entry/common.c:80
 entry_SYSCALL_64_after_hwframe+0x6e/0xd8
RIP: 0033:0x7ff155af753d
Code: 00 c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d 1b 79 2c 00 f7 d8 64 89 01 48
RSP: 002b:00007ffc006e2568 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 00007ff155af753d
RDX: 00000000200000c0 RSI: 00000000c040583c RDI: 0000000000000003
RBP: 00000000ffffffff R08: 00000000004010c0 R09: 00000000004010c0
R10: 00000000004010c0 R11: 0000000000000246 R12: 0000000000400cb0
R13: 00007ffc006e2670 R14: 0000000000000000 R15: 0000000000000000
 </TASK>

The root cause here is that xchk_stats_merge_one walks off the end of
the xchk_scrub_stats.cs_stats array because it has been fed a garbage
value in sm->sm_type.  That occurs because I put the xchk_stats_merge
in the wrong place -- it should have been after the last xchk_teardown
call on our way out of xfs_scrub_metadata because we only call the
teardown function if we called the setup function, and we don't call the
setup functions if the inputs are obviously garbage.

Thanks to Harshit for triaging the bug reports and bringing this to my
attention.

Fixes: d7a74cad8f ("xfs: track usage statistics of online fsck")
Reported-by: Harshit Mogalapalli <harshit.m.mogalapalli@oracle.com>
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Dave Chinner <dchinner@redhat.com>
2023-09-12 10:31:08 -07:00

408 lines
9 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Oracle. All Rights Reserved.
* Author: Darrick J. Wong <djwong@kernel.org>
*/
#include "xfs.h"
#include "xfs_fs.h"
#include "xfs_shared.h"
#include "xfs_format.h"
#include "xfs_trans_resv.h"
#include "xfs_mount.h"
#include "xfs_sysfs.h"
#include "xfs_btree.h"
#include "xfs_super.h"
#include "scrub/scrub.h"
#include "scrub/stats.h"
#include "scrub/trace.h"
struct xchk_scrub_stats {
/* all 32-bit counters here */
/* checking stats */
uint32_t invocations;
uint32_t clean;
uint32_t corrupt;
uint32_t preen;
uint32_t xfail;
uint32_t xcorrupt;
uint32_t incomplete;
uint32_t warning;
uint32_t retries;
/* repair stats */
uint32_t repair_invocations;
uint32_t repair_success;
/* all 64-bit items here */
/* runtimes */
uint64_t checktime_us;
uint64_t repairtime_us;
/* non-counter state must go at the end for clearall */
spinlock_t css_lock;
};
struct xchk_stats {
struct dentry *cs_debugfs;
struct xchk_scrub_stats cs_stats[XFS_SCRUB_TYPE_NR];
};
static struct xchk_stats global_stats;
static const char *name_map[XFS_SCRUB_TYPE_NR] = {
[XFS_SCRUB_TYPE_SB] = "sb",
[XFS_SCRUB_TYPE_AGF] = "agf",
[XFS_SCRUB_TYPE_AGFL] = "agfl",
[XFS_SCRUB_TYPE_AGI] = "agi",
[XFS_SCRUB_TYPE_BNOBT] = "bnobt",
[XFS_SCRUB_TYPE_CNTBT] = "cntbt",
[XFS_SCRUB_TYPE_INOBT] = "inobt",
[XFS_SCRUB_TYPE_FINOBT] = "finobt",
[XFS_SCRUB_TYPE_RMAPBT] = "rmapbt",
[XFS_SCRUB_TYPE_REFCNTBT] = "refcountbt",
[XFS_SCRUB_TYPE_INODE] = "inode",
[XFS_SCRUB_TYPE_BMBTD] = "bmapbtd",
[XFS_SCRUB_TYPE_BMBTA] = "bmapbta",
[XFS_SCRUB_TYPE_BMBTC] = "bmapbtc",
[XFS_SCRUB_TYPE_DIR] = "directory",
[XFS_SCRUB_TYPE_XATTR] = "xattr",
[XFS_SCRUB_TYPE_SYMLINK] = "symlink",
[XFS_SCRUB_TYPE_PARENT] = "parent",
[XFS_SCRUB_TYPE_RTBITMAP] = "rtbitmap",
[XFS_SCRUB_TYPE_RTSUM] = "rtsummary",
[XFS_SCRUB_TYPE_UQUOTA] = "usrquota",
[XFS_SCRUB_TYPE_GQUOTA] = "grpquota",
[XFS_SCRUB_TYPE_PQUOTA] = "prjquota",
[XFS_SCRUB_TYPE_FSCOUNTERS] = "fscounters",
};
/* Format the scrub stats into a text buffer, similar to pcp style. */
STATIC ssize_t
xchk_stats_format(
struct xchk_stats *cs,
char *buf,
size_t remaining)
{
struct xchk_scrub_stats *css = &cs->cs_stats[0];
unsigned int i;
ssize_t copied = 0;
int ret = 0;
for (i = 0; i < XFS_SCRUB_TYPE_NR; i++, css++) {
if (!name_map[i])
continue;
ret = scnprintf(buf, remaining,
"%s %u %u %u %u %u %u %u %u %u %llu %u %u %llu\n",
name_map[i],
(unsigned int)css->invocations,
(unsigned int)css->clean,
(unsigned int)css->corrupt,
(unsigned int)css->preen,
(unsigned int)css->xfail,
(unsigned int)css->xcorrupt,
(unsigned int)css->incomplete,
(unsigned int)css->warning,
(unsigned int)css->retries,
(unsigned long long)css->checktime_us,
(unsigned int)css->repair_invocations,
(unsigned int)css->repair_success,
(unsigned long long)css->repairtime_us);
if (ret <= 0)
break;
remaining -= ret;
copied += ret;
buf += ret;
}
return copied > 0 ? copied : ret;
}
/* Estimate the worst case buffer size required to hold the whole report. */
STATIC size_t
xchk_stats_estimate_bufsize(
struct xchk_stats *cs)
{
struct xchk_scrub_stats *css = &cs->cs_stats[0];
unsigned int i;
size_t field_width;
size_t ret = 0;
/* 4294967296 plus one space for each u32 field */
field_width = 11 * (offsetof(struct xchk_scrub_stats, checktime_us) /
sizeof(uint32_t));
/* 18446744073709551615 plus one space for each u64 field */
field_width += 21 * ((offsetof(struct xchk_scrub_stats, css_lock) -
offsetof(struct xchk_scrub_stats, checktime_us)) /
sizeof(uint64_t));
for (i = 0; i < XFS_SCRUB_TYPE_NR; i++, css++) {
if (!name_map[i])
continue;
/* name plus one space */
ret += 1 + strlen(name_map[i]);
/* all fields, plus newline */
ret += field_width + 1;
}
return ret;
}
/* Clear all counters. */
STATIC void
xchk_stats_clearall(
struct xchk_stats *cs)
{
struct xchk_scrub_stats *css = &cs->cs_stats[0];
unsigned int i;
for (i = 0; i < XFS_SCRUB_TYPE_NR; i++, css++) {
spin_lock(&css->css_lock);
memset(css, 0, offsetof(struct xchk_scrub_stats, css_lock));
spin_unlock(&css->css_lock);
}
}
#define XFS_SCRUB_OFLAG_UNCLEAN (XFS_SCRUB_OFLAG_CORRUPT | \
XFS_SCRUB_OFLAG_PREEN | \
XFS_SCRUB_OFLAG_XFAIL | \
XFS_SCRUB_OFLAG_XCORRUPT | \
XFS_SCRUB_OFLAG_INCOMPLETE | \
XFS_SCRUB_OFLAG_WARNING)
STATIC void
xchk_stats_merge_one(
struct xchk_stats *cs,
const struct xfs_scrub_metadata *sm,
const struct xchk_stats_run *run)
{
struct xchk_scrub_stats *css;
if (sm->sm_type >= XFS_SCRUB_TYPE_NR) {
ASSERT(sm->sm_type < XFS_SCRUB_TYPE_NR);
return;
}
css = &cs->cs_stats[sm->sm_type];
spin_lock(&css->css_lock);
css->invocations++;
if (!(sm->sm_flags & XFS_SCRUB_OFLAG_UNCLEAN))
css->clean++;
if (sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
css->corrupt++;
if (sm->sm_flags & XFS_SCRUB_OFLAG_PREEN)
css->preen++;
if (sm->sm_flags & XFS_SCRUB_OFLAG_XFAIL)
css->xfail++;
if (sm->sm_flags & XFS_SCRUB_OFLAG_XCORRUPT)
css->xcorrupt++;
if (sm->sm_flags & XFS_SCRUB_OFLAG_INCOMPLETE)
css->incomplete++;
if (sm->sm_flags & XFS_SCRUB_OFLAG_WARNING)
css->warning++;
css->retries += run->retries;
css->checktime_us += howmany_64(run->scrub_ns, NSEC_PER_USEC);
if (run->repair_attempted)
css->repair_invocations++;
if (run->repair_succeeded)
css->repair_success++;
css->repairtime_us += howmany_64(run->repair_ns, NSEC_PER_USEC);
spin_unlock(&css->css_lock);
}
/* Merge these scrub-run stats into the global and mount stat data. */
void
xchk_stats_merge(
struct xfs_mount *mp,
const struct xfs_scrub_metadata *sm,
const struct xchk_stats_run *run)
{
xchk_stats_merge_one(&global_stats, sm, run);
xchk_stats_merge_one(mp->m_scrub_stats, sm, run);
}
/* debugfs boilerplate */
static ssize_t
xchk_scrub_stats_read(
struct file *file,
char __user *ubuf,
size_t count,
loff_t *ppos)
{
struct xchk_stats *cs = file->private_data;
char *buf;
size_t bufsize;
ssize_t avail, ret;
/*
* This generates stringly snapshot of all the scrub counters, so we
* do not want userspace to receive garbled text from multiple calls.
* If the file position is greater than 0, return a short read.
*/
if (*ppos > 0)
return 0;
bufsize = xchk_stats_estimate_bufsize(cs);
buf = kvmalloc(bufsize, XCHK_GFP_FLAGS);
if (!buf)
return -ENOMEM;
avail = xchk_stats_format(cs, buf, bufsize);
if (avail < 0) {
ret = avail;
goto out;
}
ret = simple_read_from_buffer(ubuf, count, ppos, buf, avail);
out:
kvfree(buf);
return ret;
}
static const struct file_operations scrub_stats_fops = {
.open = simple_open,
.read = xchk_scrub_stats_read,
};
static ssize_t
xchk_clear_scrub_stats_write(
struct file *file,
const char __user *ubuf,
size_t count,
loff_t *ppos)
{
struct xchk_stats *cs = file->private_data;
unsigned int val;
int ret;
ret = kstrtouint_from_user(ubuf, count, 0, &val);
if (ret)
return ret;
if (val != 1)
return -EINVAL;
xchk_stats_clearall(cs);
return count;
}
static const struct file_operations clear_scrub_stats_fops = {
.open = simple_open,
.write = xchk_clear_scrub_stats_write,
};
/* Initialize the stats object. */
STATIC int
xchk_stats_init(
struct xchk_stats *cs,
struct xfs_mount *mp)
{
struct xchk_scrub_stats *css = &cs->cs_stats[0];
unsigned int i;
for (i = 0; i < XFS_SCRUB_TYPE_NR; i++, css++)
spin_lock_init(&css->css_lock);
return 0;
}
/* Connect the stats object to debugfs. */
void
xchk_stats_register(
struct xchk_stats *cs,
struct dentry *parent)
{
if (!parent)
return;
cs->cs_debugfs = xfs_debugfs_mkdir("scrub", parent);
if (!cs->cs_debugfs)
return;
debugfs_create_file("stats", 0644, cs->cs_debugfs, cs,
&scrub_stats_fops);
debugfs_create_file("clear_stats", 0400, cs->cs_debugfs, cs,
&clear_scrub_stats_fops);
}
/* Free all resources related to the stats object. */
STATIC int
xchk_stats_teardown(
struct xchk_stats *cs)
{
return 0;
}
/* Disconnect the stats object from debugfs. */
void
xchk_stats_unregister(
struct xchk_stats *cs)
{
debugfs_remove(cs->cs_debugfs);
}
/* Initialize global stats and register them */
int __init
xchk_global_stats_setup(
struct dentry *parent)
{
int error;
error = xchk_stats_init(&global_stats, NULL);
if (error)
return error;
xchk_stats_register(&global_stats, parent);
return 0;
}
/* Unregister global stats and tear them down */
void
xchk_global_stats_teardown(void)
{
xchk_stats_unregister(&global_stats);
xchk_stats_teardown(&global_stats);
}
/* Allocate per-mount stats */
int
xchk_mount_stats_alloc(
struct xfs_mount *mp)
{
struct xchk_stats *cs;
int error;
cs = kvzalloc(sizeof(struct xchk_stats), GFP_KERNEL);
if (!cs)
return -ENOMEM;
error = xchk_stats_init(cs, mp);
if (error)
goto out_free;
mp->m_scrub_stats = cs;
return 0;
out_free:
kvfree(cs);
return error;
}
/* Free per-mount stats */
void
xchk_mount_stats_free(
struct xfs_mount *mp)
{
xchk_stats_teardown(mp->m_scrub_stats);
kvfree(mp->m_scrub_stats);
mp->m_scrub_stats = NULL;
}