[PATCH 1/3] libfs: reject casefolded names that fold to ".", "..", or empty
From: Sarthak Kukreti
Date: Fri Nov 13 2026 - 02:27:42 EST
Unicode normalization and casefolding (utf8_casefold()) strips code
points with the Default_Ignorable_Code_Point property, such as U+202E
(RIGHT-TO-LEFT OVERRIDE) or U+200B (ZERO WIDTH SPACE). As a result:
1. A filename consisting of "." or ".." followed by ignorable characters
(e.g. ".\xe2\x80\xae" or "..\xe2\x80\xae") bypasses VFS special-entry
handling during path walk, but casefolds to "." or "..".
2. A filename consisting solely of ignorable characters (e.g.
"\xe2\x80\xae") casefolds to the empty string "" (length 0), causing
any two distinct ignorable-only filenames to compare and hash as
equal.
Update generic_ci_d_hash(), generic_ci_d_compare(), generic_ci_match(),
and generic_ci_validate_strict_name() to reject case-insensitive
hashing, matching, and creation of filenames that casefold to "", ".",
or "..", while preserving exact byte-equality matches (such as NFS
export lookups of literal ".."). Using a 3-byte stack buffer with
utf8_casefold() detects all three cases in O(1) space without heap
allocations.
Fixes: c843843e714c ("fs: Add standard casefolding support")
Co-developed-by: Dmitry Torokhov <dtor@xxxxxxxxxx>
Signed-off-by: Dmitry Torokhov <dtor@xxxxxxxxxx>
Signed-off-by: Sarthak Kukreti <sarthakkukreti@xxxxxxxxxx>
---
fs/libfs.c | 50 ++++++++++++++++++++++++++++++++++++++++++----
include/linux/fs.h | 31 ++++++++++++++++------------
2 files changed, 64 insertions(+), 17 deletions(-)
diff --git a/fs/libfs.c b/fs/libfs.c
index 27d7dc16fcb0..54296dfb8444 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -1801,8 +1801,11 @@ int generic_ci_d_compare(const struct dentry *dentry, unsigned int len,
{
const struct dentry *parent;
const struct inode *dir;
+ const struct unicode_map *um;
union shortname_store strbuf;
+ unsigned char buf[3];
struct qstr qstr;
+ int cf_len;
/*
* Attempt a case-sensitive match first. It is cheaper and
@@ -1821,6 +1824,19 @@ int generic_ci_d_compare(const struct dentry *dentry, unsigned int len,
if (!dir || !IS_CASEFOLDED(dir))
return 1;
+ if (name_is_dot_dotdot(str, len) ||
+ name_is_dot_dotdot(name->name, name->len))
+ return 1;
+
+ /*
+ * utf8_strncasecmp() only matches if both names casefold to the same
+ * byte sequence, so checking @name covers @qstr as well.
+ */
+ um = dentry->d_sb->s_encoding;
+ cf_len = utf8_casefold(um, name, buf, sizeof(buf));
+ if (cf_len == 0 || (cf_len > 0 && name_is_dot_dotdot(buf, cf_len)))
+ return 1;
+
qstr.len = len;
qstr.name = str;
/*
@@ -1839,7 +1855,7 @@ int generic_ci_d_compare(const struct dentry *dentry, unsigned int len,
barrier();
}
- return utf8_strncasecmp(dentry->d_sb->s_encoding, name, &qstr);
+ return utf8_strncasecmp(um, name, &qstr);
}
EXPORT_SYMBOL(generic_ci_d_compare);
@@ -1855,13 +1871,21 @@ int generic_ci_d_hash(const struct dentry *dentry, struct qstr *str)
const struct inode *dir = READ_ONCE(dentry->d_inode);
struct super_block *sb = dentry->d_sb;
const struct unicode_map *um = sb->s_encoding;
+ unsigned char buf[3];
int ret;
if (!dir || !IS_CASEFOLDED(dir))
return 0;
ret = utf8_casefold_hash(um, dentry, str);
- if (ret < 0 && sb_has_strict_encoding(sb))
+ if (ret < 0) {
+ if (sb_has_strict_encoding(sb))
+ return -EINVAL;
+ return 0;
+ }
+
+ ret = utf8_casefold(um, str, buf, sizeof(buf));
+ if (ret == 0 || (ret > 0 && name_is_dot_dotdot(buf, ret)))
return -EINVAL;
return 0;
}
@@ -1934,10 +1958,28 @@ int generic_ci_match(const struct inode *parent,
!memcmp(name->name, dirent.name, dirent.len))
goto out;
- if (folded_name->name)
+ if (name_is_dot_dotdot(dirent.name, dirent.len) ||
+ name_is_dot_dotdot(name->name, name->len) ||
+ (folded_name->name &&
+ (!folded_name->len ||
+ name_is_dot_dotdot(folded_name->name, folded_name->len)))) {
+ res = 1;
+ goto out;
+ }
+
+ if (folded_name->name) {
res = utf8_strncasecmp_folded(um, folded_name, &dirent);
- else
+ } else {
+ unsigned char buf[3];
+ int cf_len = utf8_casefold(um, name, buf, sizeof(buf));
+
+ if (cf_len == 0 ||
+ (cf_len > 0 && name_is_dot_dotdot(buf, cf_len))) {
+ res = 1;
+ goto out;
+ }
res = utf8_strncasecmp(um, name, &dirent);
+ }
out:
kfree(decrypted_name.name);
diff --git a/include/linux/fs.h b/include/linux/fs.h
index f9d1e05e8ae6..84f5ce2bb9ad 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -3356,28 +3356,26 @@ int generic_ci_d_compare(const struct dentry *dentry, unsigned int len,
* generic_ci_validate_strict_name - Check if a given name is suitable
* for a directory
*
- * This functions checks if the proposed filename is valid for the
- * parent directory. That means that only valid UTF-8 filenames will be
- * accepted for casefold directories from filesystems created with the
- * strict encoding flag. That also means that any name will be
- * accepted for directories that doesn't have casefold enabled, or
- * aren't being strict with the encoding.
+ * This function checks if the proposed filename is valid for the
+ * parent directory. Names that casefold to "", ".", or ".." are
+ * rejected for all casefolded directories. When the filesystem is
+ * created with the strict encoding flag, only valid UTF-8 filenames
+ * are accepted for casefolded directories.
*
* @dir: inode of the directory where the new file will be created
* @name: name of the new file
*
* Return:
- * * True: if the filename is suitable for this directory. It can be
- * true if a given name is not suitable for a strict encoding
- * directory, but the directory being used isn't strict
- * * False if the filename isn't suitable for this directory. This only
- * happens when a directory is casefolded and the filesystem is strict
- * about its encoding.
+ * * True: if the filename is suitable for this directory.
+ * * False: if the filename isn't suitable for this directory.
*/
static inline bool generic_ci_validate_strict_name(struct inode *dir,
const struct qstr *name)
{
- if (!IS_CASEFOLDED(dir) || !sb_has_strict_encoding(dir->i_sb))
+ unsigned char buf[3];
+ int len;
+
+ if (!IS_CASEFOLDED(dir))
return true;
/*
@@ -3387,6 +3385,13 @@ static inline bool generic_ci_validate_strict_name(struct inode *dir,
if (WARN_ON_ONCE(!dir->i_sb->s_encoding))
return true;
+ len = utf8_casefold(dir->i_sb->s_encoding, name, buf, sizeof(buf));
+ if (len == 0 || (len > 0 && name_is_dot_dotdot(buf, len)))
+ return false;
+
+ if (!sb_has_strict_encoding(dir->i_sb))
+ return true;
+
return !utf8_validate(dir->i_sb->s_encoding, name);
}
#else
--
2.56.0.611.g1030f0b940-goog