exfat: add cluster chain loop check for dir

An infinite loop may occur if the following conditions occur due to
file system corruption.

(1) Condition for exfat_count_dir_entries() to loop infinitely.
    - The cluster chain includes a loop.
    - There is no UNUSED entry in the cluster chain.

(2) Condition for exfat_create_upcase_table() to loop infinitely.
    - The cluster chain of the root directory includes a loop.
    - There are no UNUSED entry and up-case table entry in the cluster
      chain of the root directory.

(3) Condition for exfat_load_bitmap() to loop infinitely.
    - The cluster chain of the root directory includes a loop.
    - There are no UNUSED entry and bitmap entry in the cluster chain
      of the root directory.

(4) Condition for exfat_find_dir_entry() to loop infinitely.
    - The cluster chain includes a loop.
    - The unused directory entries were exhausted by some operation.

(5) Condition for exfat_check_dir_empty() to loop infinitely.
    - The cluster chain includes a loop.
    - The unused directory entries were exhausted by some operation.
    - All files and sub-directories under the directory are deleted.

This commit adds checks to break the above infinite loop.

Signed-off-by: Yuezhang Mo <Yuezhang.Mo@sony.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
This commit is contained in:
Yuezhang Mo 2025-03-18 17:00:49 +08:00 committed by Namjae Jeon
parent 2f2d42a17b
commit 99f9a97dce
4 changed files with 48 additions and 11 deletions

View file

@ -996,6 +996,7 @@ int exfat_find_dir_entry(struct super_block *sb, struct exfat_inode_info *ei,
struct exfat_hint_femp candi_empty; struct exfat_hint_femp candi_empty;
struct exfat_sb_info *sbi = EXFAT_SB(sb); struct exfat_sb_info *sbi = EXFAT_SB(sb);
int num_entries = exfat_calc_num_entries(p_uniname); int num_entries = exfat_calc_num_entries(p_uniname);
unsigned int clu_count = 0;
if (num_entries < 0) if (num_entries < 0)
return num_entries; return num_entries;
@ -1133,6 +1134,10 @@ rewind:
} else { } else {
if (exfat_get_next_cluster(sb, &clu.dir)) if (exfat_get_next_cluster(sb, &clu.dir))
return -EIO; return -EIO;
/* break if the cluster chain includes a loop */
if (unlikely(++clu_count > EXFAT_DATA_CLUSTER_COUNT(sbi)))
goto not_found;
} }
} }
@ -1195,6 +1200,7 @@ int exfat_count_dir_entries(struct super_block *sb, struct exfat_chain *p_dir)
int i, count = 0; int i, count = 0;
int dentries_per_clu; int dentries_per_clu;
unsigned int entry_type; unsigned int entry_type;
unsigned int clu_count = 0;
struct exfat_chain clu; struct exfat_chain clu;
struct exfat_dentry *ep; struct exfat_dentry *ep;
struct exfat_sb_info *sbi = EXFAT_SB(sb); struct exfat_sb_info *sbi = EXFAT_SB(sb);
@ -1227,6 +1233,12 @@ int exfat_count_dir_entries(struct super_block *sb, struct exfat_chain *p_dir)
} else { } else {
if (exfat_get_next_cluster(sb, &(clu.dir))) if (exfat_get_next_cluster(sb, &(clu.dir)))
return -EIO; return -EIO;
if (unlikely(++clu_count > sbi->used_clusters)) {
exfat_fs_error(sb, "FAT or bitmap is corrupted");
return -EIO;
}
} }
} }

View file

@ -490,5 +490,15 @@ int exfat_count_num_clusters(struct super_block *sb,
} }
*ret_count = count; *ret_count = count;
/*
* since exfat_count_used_clusters() is not called, sbi->used_clusters
* cannot be used here.
*/
if (unlikely(i == sbi->num_clusters && clu != EXFAT_EOF_CLUSTER)) {
exfat_fs_error(sb, "The cluster chain has a loop");
return -EIO;
}
return 0; return 0;
} }

View file

@ -890,6 +890,7 @@ static int exfat_check_dir_empty(struct super_block *sb,
{ {
int i, dentries_per_clu; int i, dentries_per_clu;
unsigned int type; unsigned int type;
unsigned int clu_count = 0;
struct exfat_chain clu; struct exfat_chain clu;
struct exfat_dentry *ep; struct exfat_dentry *ep;
struct exfat_sb_info *sbi = EXFAT_SB(sb); struct exfat_sb_info *sbi = EXFAT_SB(sb);
@ -926,6 +927,10 @@ static int exfat_check_dir_empty(struct super_block *sb,
} else { } else {
if (exfat_get_next_cluster(sb, &(clu.dir))) if (exfat_get_next_cluster(sb, &(clu.dir)))
return -EIO; return -EIO;
/* break if the cluster chain includes a loop */
if (unlikely(++clu_count > EXFAT_DATA_CLUSTER_COUNT(sbi)))
break;
} }
} }

View file

@ -341,13 +341,12 @@ static void exfat_hash_init(struct super_block *sb)
INIT_HLIST_HEAD(&sbi->inode_hashtable[i]); INIT_HLIST_HEAD(&sbi->inode_hashtable[i]);
} }
static int exfat_read_root(struct inode *inode) static int exfat_read_root(struct inode *inode, struct exfat_chain *root_clu)
{ {
struct super_block *sb = inode->i_sb; struct super_block *sb = inode->i_sb;
struct exfat_sb_info *sbi = EXFAT_SB(sb); struct exfat_sb_info *sbi = EXFAT_SB(sb);
struct exfat_inode_info *ei = EXFAT_I(inode); struct exfat_inode_info *ei = EXFAT_I(inode);
struct exfat_chain cdir; int num_subdirs;
int num_subdirs, num_clu = 0;
exfat_chain_set(&ei->dir, sbi->root_dir, 0, ALLOC_FAT_CHAIN); exfat_chain_set(&ei->dir, sbi->root_dir, 0, ALLOC_FAT_CHAIN);
ei->entry = -1; ei->entry = -1;
@ -360,12 +359,9 @@ static int exfat_read_root(struct inode *inode)
ei->hint_stat.clu = sbi->root_dir; ei->hint_stat.clu = sbi->root_dir;
ei->hint_femp.eidx = EXFAT_HINT_NONE; ei->hint_femp.eidx = EXFAT_HINT_NONE;
exfat_chain_set(&cdir, sbi->root_dir, 0, ALLOC_FAT_CHAIN); i_size_write(inode, EXFAT_CLU_TO_B(root_clu->size, sbi));
if (exfat_count_num_clusters(sb, &cdir, &num_clu))
return -EIO;
i_size_write(inode, num_clu << sbi->cluster_size_bits);
num_subdirs = exfat_count_dir_entries(sb, &cdir); num_subdirs = exfat_count_dir_entries(sb, root_clu);
if (num_subdirs < 0) if (num_subdirs < 0)
return -EIO; return -EIO;
set_nlink(inode, num_subdirs + EXFAT_MIN_SUBDIR); set_nlink(inode, num_subdirs + EXFAT_MIN_SUBDIR);
@ -578,7 +574,8 @@ static int exfat_verify_boot_region(struct super_block *sb)
} }
/* mount the file system volume */ /* mount the file system volume */
static int __exfat_fill_super(struct super_block *sb) static int __exfat_fill_super(struct super_block *sb,
struct exfat_chain *root_clu)
{ {
int ret; int ret;
struct exfat_sb_info *sbi = EXFAT_SB(sb); struct exfat_sb_info *sbi = EXFAT_SB(sb);
@ -595,6 +592,18 @@ static int __exfat_fill_super(struct super_block *sb)
goto free_bh; goto free_bh;
} }
/*
* Call exfat_count_num_cluster() before searching for up-case and
* bitmap directory entries to avoid infinite loop if they are missing
* and the cluster chain includes a loop.
*/
exfat_chain_set(root_clu, sbi->root_dir, 0, ALLOC_FAT_CHAIN);
ret = exfat_count_num_clusters(sb, root_clu, &root_clu->size);
if (ret) {
exfat_err(sb, "failed to count the number of clusters in root");
goto free_bh;
}
ret = exfat_create_upcase_table(sb); ret = exfat_create_upcase_table(sb);
if (ret) { if (ret) {
exfat_err(sb, "failed to load upcase table"); exfat_err(sb, "failed to load upcase table");
@ -627,6 +636,7 @@ static int exfat_fill_super(struct super_block *sb, struct fs_context *fc)
struct exfat_sb_info *sbi = sb->s_fs_info; struct exfat_sb_info *sbi = sb->s_fs_info;
struct exfat_mount_options *opts = &sbi->options; struct exfat_mount_options *opts = &sbi->options;
struct inode *root_inode; struct inode *root_inode;
struct exfat_chain root_clu;
int err; int err;
if (opts->allow_utime == (unsigned short)-1) if (opts->allow_utime == (unsigned short)-1)
@ -645,7 +655,7 @@ static int exfat_fill_super(struct super_block *sb, struct fs_context *fc)
sb->s_time_min = EXFAT_MIN_TIMESTAMP_SECS; sb->s_time_min = EXFAT_MIN_TIMESTAMP_SECS;
sb->s_time_max = EXFAT_MAX_TIMESTAMP_SECS; sb->s_time_max = EXFAT_MAX_TIMESTAMP_SECS;
err = __exfat_fill_super(sb); err = __exfat_fill_super(sb, &root_clu);
if (err) { if (err) {
exfat_err(sb, "failed to recognize exfat type"); exfat_err(sb, "failed to recognize exfat type");
goto check_nls_io; goto check_nls_io;
@ -680,7 +690,7 @@ static int exfat_fill_super(struct super_block *sb, struct fs_context *fc)
root_inode->i_ino = EXFAT_ROOT_INO; root_inode->i_ino = EXFAT_ROOT_INO;
inode_set_iversion(root_inode, 1); inode_set_iversion(root_inode, 1);
err = exfat_read_root(root_inode); err = exfat_read_root(root_inode, &root_clu);
if (err) { if (err) {
exfat_err(sb, "failed to initialize root inode"); exfat_err(sb, "failed to initialize root inode");
goto put_inode; goto put_inode;