struct ext4_tune_sb_params exposes last_check_time as a __u64, and the superblock stores the time of the last check as a 40-bit value split between s_lastcheck and s_lastcheck_hi. However, EXT4_IOC_GET_TUNE_SB_PARAM only reports s_lastcheck, and EXT4_IOC_SET_TUNE_SB_PARAM only writes s_lastcheck, leaving any existing s_lastcheck_hi in place. So timestamps past 2106 are truncated when read, and setting the last check time on a file system whose s_lastcheck_hi is non-zero results in a timestamp that is off by a multiple of 2^32 seconds, which confuses the time-based fsck check in ext4_setup_super() and e2fsck. Read and write s_lastcheck_hi along with s_lastcheck, and reject values which don't fit in the 40-bit on-disk representation. Fixes: 04a91570ac67 ("ext4: implemet new ioctls to set and get superblock parameters") Signed-off-by: Theodore Ts'o --- fs/ext4/ioctl-test.c | 62 ++++++++++++++++++++++++++++++++++++++++++++ fs/ext4/ioctl.c | 12 +++++++-- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/fs/ext4/ioctl-test.c b/fs/ext4/ioctl-test.c index fd7b4ff2cd81..831b7e032fad 100644 --- a/fs/ext4/ioctl-test.c +++ b/fs/ext4/ioctl-test.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include @@ -889,6 +891,65 @@ static void test_tune_sb_feature_prereqs(struct kunit *test) KUNIT_EXPECT_EQ(test, sbi->s_csum_seed, seed); } +/* + * Verify that last_check_time is validated against, stored to, and + * reported from the 40-bit s_lastcheck / s_lastcheck_hi timestamp. + */ +static void test_tune_sb_last_check_time(struct kunit *test) +{ + struct ioctl_test_ctx *ctx = test->priv; + struct super_block *sb = ctx->sb; + struct ext4_sb_info *sbi = &ctx->sbi; + struct ext4_super_block *es = ctx->es; + struct ext4_tune_sb_params params; + struct vfsmount mnt = { + .mnt_root = sb->s_root, + .mnt_sb = sb, + .mnt_idmap = &nop_mnt_idmap, + }; + struct file filp = { + .f_inode = d_inode(sb->s_root), + .f_path = { + .mnt = &mnt, + .dentry = sb->s_root, + }, + .f_mode = FMODE_READ, + }; + unsigned long uaddr; + + memset(¶ms, 0, sizeof(params)); + params.set_flags = EXT4_TUNE_FL_LAST_CHECK_TIME; + params.last_check_time = 1ULL << 40; + KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, ¶ms), + -EINVAL); + + params.last_check_time = 0x123456789aULL; + KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, ¶ms), + 0); + es->s_lastcheck_hi = 0xff; + ext4_sb_setparams_test(sbi, es, ¶ms); + KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_lastcheck), 0x3456789aU); + KUNIT_EXPECT_EQ(test, es->s_lastcheck_hi, 0x12); + + /* EXT4_IOC_GET_TUNE_SB_PARAM reports all 40 bits */ + uaddr = kunit_vm_mmap(test, NULL, 0, PAGE_SIZE, + PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_PRIVATE, 0); + KUNIT_ASSERT_NE_MSG(test, uaddr, 0UL, "Could not create user mm"); + KUNIT_ASSERT_LT_MSG(test, uaddr, (unsigned long)TASK_SIZE, + "Failed to allocate user memory"); + + KUNIT_ASSERT_EQ(test, + ext4_ioctl(&filp, EXT4_IOC_GET_TUNE_SB_PARAM, uaddr), + 0); + memset(¶ms, 0, sizeof(params)); + KUNIT_ASSERT_EQ(test, + copy_from_user(¶ms, (void __user *)uaddr, + sizeof(params)), + 0UL); + KUNIT_EXPECT_EQ(test, params.last_check_time, 0x123456789aULL); +} + /* * Verify ext4_update_backup_sb, ext4_update_superblocks_fn, and * ext4_update_overhead across primary + 4 sparse_super backup groups (1, 3, @@ -1398,6 +1459,7 @@ static struct kunit_case ioctl_test_cases[] = { KUNIT_CASE(test_tune_sb_mount_opts_guard), KUNIT_CASE(test_tune_sb_dir_index_hash_state), KUNIT_CASE(test_tune_sb_feature_prereqs), + KUNIT_CASE(test_tune_sb_last_check_time), KUNIT_CASE(test_update_superblocks_and_overhead), KUNIT_CASE(test_update_backup_sb_bad_csum), KUNIT_CASE(test_swap_inode_data_and_seed), diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c index 8bd4958217e0..9e2bde83a033 100644 --- a/fs/ext4/ioctl.c +++ b/fs/ext4/ioctl.c @@ -1308,7 +1308,8 @@ static int ext4_ioctl_get_tune_sb(struct ext4_sb_info *sbi, ret.mnt_count = le16_to_cpu(es->s_mnt_count); ret.max_mnt_count = le16_to_cpu(es->s_max_mnt_count); ret.checkinterval = le32_to_cpu(es->s_checkinterval); - ret.last_check_time = le32_to_cpu(es->s_lastcheck); + ret.last_check_time = ((u64)es->s_lastcheck_hi << 32) | + le32_to_cpu(es->s_lastcheck); ret.reserved_blocks = ext4_r_blocks_count(es); ret.blocks_count = ext4_blocks_count(es); ret.reserved_uid = ext4_get_resuid(es); @@ -1347,8 +1348,10 @@ static void ext4_sb_setparams(struct ext4_sb_info *sbi, es->s_max_mnt_count = cpu_to_le16(params->max_mnt_count); if (params->set_flags & EXT4_TUNE_FL_CHECKINTRVAL) es->s_checkinterval = cpu_to_le32(params->checkinterval); - if (params->set_flags & EXT4_TUNE_FL_LAST_CHECK_TIME) + if (params->set_flags & EXT4_TUNE_FL_LAST_CHECK_TIME) { es->s_lastcheck = cpu_to_le32(params->last_check_time); + es->s_lastcheck_hi = upper_32_bits(params->last_check_time); + } if (params->set_flags & EXT4_TUNE_FL_RESERVED_BLOCKS) { ext4_fsblk_t blk = params->reserved_blocks; @@ -1432,6 +1435,11 @@ static int ext4_validate_tune_sb_params(struct super_block *sb, (params->errors_behavior > EXT4_ERRORS_PANIC)) return -EINVAL; + /* s_lastcheck{,_hi} hold a 40-bit timestamp */ + if ((params->set_flags & EXT4_TUNE_FL_LAST_CHECK_TIME) && + (params->last_check_time >> 40)) + return -EINVAL; + if ((params->set_flags & EXT4_TUNE_FL_RESERVED_BLOCKS) && (params->reserved_blocks > ext4_blocks_count(sbi->s_es) / 2)) return -EINVAL; -- 2.56.0.542.g3ef89b7020-goog