[PATCH 02/19] libext2fs: create link count adjustment helpers for dir_nlink

Darrick J. Wong
2025-11-06T22:31:37Z

Create some helpers to deal with link count adjustments for directories
on dir_nlink filesystems that become large enough to have an htree
index. In other words, fix the problem that creating a new child
subdirectory can overflow the link count of the parent directory.

The unused library functions created by this patch will be used in the
next patch to fix problems with fuse2fs.

Cc: <linux-ext4@vger.kernel.org> # v1.40
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
---
lib/ext2fs/ext2fs.h | 5 ++++
debian/libext2fs2t64.symbols | 5 ++++
lib/ext2fs/link.c | 49 ++++++++++++++++++++++++++++++++++++++++++
lib/ext2fs/mkdir.c | 2 +-
4 files changed, 60 insertions(+), 1 deletion(-)

diff --git a/lib/ext2fs/ext2fs.h b/lib/ext2fs/ext2fs.h
index bb2170b78d6308..dec48b80d341db 100644
--- a/lib/ext2fs/ext2fs.h
+++ b/lib/ext2fs/ext2fs.h
@@ -1840,6 +1840,11 @@ errcode_t ext2fs_link(ext2_filsys fs, ext2_ino_t dir, const char *name,
ext2_ino_t ino, int flags);
errcode_t ext2fs_unlink(ext2_filsys fs, ext2_ino_t dir, const char *name,
ext2_ino_t ino, int flags);
+int ext2fs_dir_is_dx(ext2_filsys fs, const struct ext2_inode *inode);
+void ext2fs_inc_nlink(ext2_filsys fs, struct ext2_inode *inode);
+void ext2fs_dec_nlink(struct ext2_inode *inode);
+int ext2fs_dir_link_max(ext2_filsys fs, struct ext2_inode_large *inode);
+int ext2fs_dir_link_empty(struct ext2_inode *inode);

/* symlink.c */
errcode_t ext2fs_symlink(ext2_filsys fs, ext2_ino_t parent, ext2_ino_t ino,
diff --git a/debian/libext2fs2t64.symbols b/debian/libext2fs2t64.symbols
index b4d80161f1e1b4..01f4f269a2660e 100644
--- a/debian/libext2fs2t64.symbols
+++ b/debian/libext2fs2t64.symbols
@@ -173,6 +173,7 @@ libext2fs.so.2 libext2fs2t64 #MINVER#
ext2fs_dblist_iterate@Base 1.37
ext2fs_dblist_sort2@Base 1.42
ext2fs_dblist_sort@Base 1.37
+ ext2fs_dec_nlink@Base 1.47.4
ext2fs_decode_extent@Base 1.46.0
ext2fs_default_journal_size@Base 1.40
ext2fs_default_orphan_file_blocks@Base 1.47.0
@@ -182,6 +183,9 @@ libext2fs.so.2 libext2fs2t64 #MINVER#
ext2fs_dir_block_csum_verify@Base 1.43
ext2fs_dir_iterate2@Base 1.37
ext2fs_dir_iterate@Base 1.37
+ ext2fs_dir_is_dx@Base 1.47.4
+ ext2fs_dir_link_empty@Base 1.47.4
+ ext2fs_dir_link_max@Base 1.47.4
ext2fs_dirent_csum_verify@Base 1.43
ext2fs_dirent_file_type@Base 1.43
ext2fs_dirent_has_tail@Base 1.43
@@ -376,6 +380,7 @@ libext2fs.so.2 libext2fs2t64 #MINVER#
ext2fs_image_inode_write@Base 1.37
ext2fs_image_super_read@Base 1.37
ext2fs_image_super_write@Base 1.37
+ ext2fs_inc_nlink@Base 1.47.4
ext2fs_init_csum_seed@Base 1.43
ext2fs_init_dblist@Base 1.37
ext2fs_initialize@Base 1.37
diff --git a/lib/ext2fs/link.c b/lib/ext2fs/link.c
index 83d3ea7d00c3ed..eccbf0d23b5d6c 100644
--- a/lib/ext2fs/link.c
+++ b/lib/ext2fs/link.c
@@ -919,3 +919,52 @@ errcode_t ext2fs_link(ext2_filsys fs, ext2_ino_t dir, const char *name,
}
return 0;
}
+
+/* Does this directory have an htree index? */
+int ext2fs_dir_is_dx(ext2_filsys fs, const struct ext2_inode *inode)
+{
+ return ext2fs_has_feature_dir_index(fs->super) &&
+ S_ISDIR(inode->i_mode) && (inode->i_flags & EXT2_INDEX_FL);
+}
+
+/*
+ * Set directory link count to 1 if nlinks > EXT2_LINK_MAX, or if nlinks == 2
+ * since this indicates that nlinks count was previously 1 to avoid overflowing
+ * the 16-bit i_links_count field on disk. Directories with i_nlink == 1 mean
+ * that subdirectory link counts are not being maintained accurately.
+ *
+ * The caller has already checked for i_nlink overflow in case the DIR_LINK
+ * feature is not enabled and returned -EMLINK. The is_dx() check is a proxy
+ * for checking S_ISDIR(inode) (since the INODE_INDEX feature will not be set
+ * on regular files) and to avoid creating huge/slow non-HTREE directories.
+ */
+void ext2fs_inc_nlink(ext2_filsys fs, struct ext2_inode *inode)
+{
+ inode->i_links_count++;
+
+ if (ext2fs_dir_is_dx(fs, inode) &&
+ (inode->i_links_count > EXT2_LINK_MAX || inode->i_links_count == 2))
+ inode->i_links_count = 1;
+}
+
+/*
+ * If a directory had nlink == 1, then we should let it be 1. This indicates
+ * directory has >EXT2_LINK_MAX subdirs.
+ */
+void ext2fs_dec_nlink(struct ext2_inode *inode)
+{
+ if (!S_ISDIR(inode->i_mode) || inode->i_links_count > 2)
+ inode->i_links_count--;
+}
+
+int ext2fs_dir_link_max(ext2_filsys fs, struct ext2_inode_large *inode)
+{
+ return inode->i_links_count >= EXT2_LINK_MAX &&
+ !(ext2fs_dir_is_dx(fs, EXT2_INODE(inode)) &&
+ ext2fs_has_feature_dir_nlink(fs->super));
+}
+
+int ext2fs_dir_link_empty(struct ext2_inode *inode)
+{
+ return inode->i_links_count == 2 || inode->i_links_count == 1;
+}
diff --git a/lib/ext2fs/mkdir.c b/lib/ext2fs/mkdir.c
index 45f6e9e27164d2..c0a08c88560bd2 100644
--- a/lib/ext2fs/mkdir.c
+++ b/lib/ext2fs/mkdir.c
@@ -182,7 +182,7 @@ errcode_t ext2fs_mkdir2(ext2_filsys fs, ext2_ino_t parent, ext2_ino_t ino,
retval = ext2fs_read_inode(fs, parent, &parent_inode);
if (retval)
goto cleanup;
- parent_inode.i_links_count++;
+ ext2fs_inc_nlink(fs, &parent_inode);
retval = ext2fs_write_inode(fs, parent, &parent_inode);
if (retval)
goto cleanup;