tmpfs: Add casefold lookup support

Enable casefold lookup in tmpfs, based on the encoding defined by
userspace. That means that instead of comparing byte per byte a file
name, it compares to a case-insensitive equivalent of the Unicode
string.

* Dcache handling

There's a special need when dealing with case-insensitive dentries.
First of all, we currently invalidated every negative casefold dentries.
That happens because currently VFS code has no proper support to deal
with that, giving that it could incorrectly reuse a previous filename
for a new file that has a casefold match. For instance, this could
happen:

$ mkdir DIR
$ rm -r DIR
$ mkdir dir
$ ls
DIR/

And would be perceived as inconsistency from userspace point of view,
because even that we match files in a case-insensitive manner, we still
honor whatever is the initial filename.

Along with that, tmpfs stores only the first equivalent name dentry used
in the dcache, preventing duplications of dentries in the dcache. The
d_compare() version for casefold files uses a normalized string, so the
filename under lookup will be compared to another normalized string for
the existing file, achieving a casefolded lookup.

* Enabling casefold via mount options

Most filesystems have their data stored in disk, so casefold option need
to be enabled when building a filesystem on a device (via mkfs).
However, as tmpfs is a RAM backed filesystem, there's no disk
information and thus no mkfs to store information about casefold.

For tmpfs, create casefold options for mounting. Userspace can then
enable casefold support for a mount point using:

$ mount -t tmpfs -o casefold=utf8-12.1.0 fs_name mount_dir/

Userspace must set what Unicode standard is aiming to. The available
options depends on what the kernel Unicode subsystem supports.

And for strict encoding:

$ mount -t tmpfs -o casefold=utf8-12.1.0,strict_encoding fs_name mount_dir/

Strict encoding means that tmpfs will refuse to create invalid UTF-8
sequences. When this option is not enabled, any invalid sequence will be
treated as an opaque byte sequence, ignoring the encoding thus not being
able to be looked up in a case-insensitive way.

* Check for casefold dirs on simple_lookup()

On simple_lookup(), do not create dentries for casefold directories.
Currently, VFS does not support case-insensitive negative dentries and
can create inconsistencies in the filesystem. Prevent such dentries to
being created in the first place.

Reviewed-by: Gabriel Krisman Bertazi <gabriel@krisman.be>
Reviewed-by: Gabriel Krisman Bertazi <krisman@suse.de>
Signed-off-by: André Almeida <andrealmeid@igalia.com>
Link: https://lore.kernel.org/r/20241021-tonyk-tmpfs-v8-6-f443d5814194@igalia.com
Signed-off-by: Christian Brauner <brauner@kernel.org>
This commit is contained in:
André Almeida 2024-10-21 13:37:22 -03:00 committed by Christian Brauner
parent 458532c8df
commit 58e55efd6c
No known key found for this signature in database
GPG Key ID: 91C61BC06578DCA2
2 changed files with 127 additions and 4 deletions

View File

@ -77,6 +77,10 @@ struct dentry *simple_lookup(struct inode *dir, struct dentry *dentry, unsigned
return ERR_PTR(-ENAMETOOLONG);
if (!dentry->d_sb->s_d_op)
d_set_d_op(dentry, &simple_dentry_operations);
if (IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir))
return NULL;
d_add(dentry, NULL);
return NULL;
}

View File

@ -40,6 +40,7 @@
#include <linux/fs_parser.h>
#include <linux/swapfile.h>
#include <linux/iversion.h>
#include <linux/unicode.h>
#include "swap.h"
static struct vfsmount *shm_mnt __ro_after_init;
@ -123,6 +124,10 @@ struct shmem_options {
bool noswap;
unsigned short quota_types;
struct shmem_quota_limits qlimits;
#if IS_ENABLED(CONFIG_UNICODE)
struct unicode_map *encoding;
bool strict_encoding;
#endif
#define SHMEM_SEEN_BLOCKS 1
#define SHMEM_SEEN_INODES 2
#define SHMEM_SEEN_HUGE 4
@ -3565,6 +3570,9 @@ shmem_mknod(struct mnt_idmap *idmap, struct inode *dir,
struct inode *inode;
int error;
if (!generic_ci_validate_strict_name(dir, &dentry->d_name))
return -EINVAL;
inode = shmem_get_inode(idmap, dir->i_sb, dir, mode, dev, VM_NORESERVE);
if (IS_ERR(inode))
return PTR_ERR(inode);
@ -3584,7 +3592,12 @@ shmem_mknod(struct mnt_idmap *idmap, struct inode *dir,
dir->i_size += BOGO_DIRENT_SIZE;
inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir));
inode_inc_iversion(dir);
d_instantiate(dentry, inode);
if (IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir))
d_add(dentry, inode);
else
d_instantiate(dentry, inode);
dget(dentry); /* Extra count - pin the dentry in core */
return error;
@ -3675,7 +3688,10 @@ static int shmem_link(struct dentry *old_dentry, struct inode *dir,
inc_nlink(inode);
ihold(inode); /* New dentry reference */
dget(dentry); /* Extra pinning count for the created dentry */
d_instantiate(dentry, inode);
if (IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir))
d_add(dentry, inode);
else
d_instantiate(dentry, inode);
out:
return ret;
}
@ -3695,6 +3711,14 @@ static int shmem_unlink(struct inode *dir, struct dentry *dentry)
inode_inc_iversion(dir);
drop_nlink(inode);
dput(dentry); /* Undo the count from "create" - does all the work */
/*
* For now, VFS can't deal with case-insensitive negative dentries, so
* we invalidate them
*/
if (IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir))
d_invalidate(dentry);
return 0;
}
@ -3839,7 +3863,10 @@ static int shmem_symlink(struct mnt_idmap *idmap, struct inode *dir,
dir->i_size += BOGO_DIRENT_SIZE;
inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir));
inode_inc_iversion(dir);
d_instantiate(dentry, inode);
if (IS_ENABLED(CONFIG_UNICODE) && IS_CASEFOLDED(dir))
d_add(dentry, inode);
else
d_instantiate(dentry, inode);
dget(dentry);
return 0;
@ -4192,6 +4219,9 @@ enum shmem_param {
Opt_usrquota_inode_hardlimit,
Opt_grpquota_block_hardlimit,
Opt_grpquota_inode_hardlimit,
Opt_casefold_version,
Opt_casefold,
Opt_strict_encoding,
};
static const struct constant_table shmem_param_enums_huge[] = {
@ -4223,9 +4253,54 @@ const struct fs_parameter_spec shmem_fs_parameters[] = {
fsparam_string("grpquota_block_hardlimit", Opt_grpquota_block_hardlimit),
fsparam_string("grpquota_inode_hardlimit", Opt_grpquota_inode_hardlimit),
#endif
fsparam_string("casefold", Opt_casefold_version),
fsparam_flag ("casefold", Opt_casefold),
fsparam_flag ("strict_encoding", Opt_strict_encoding),
{}
};
#if IS_ENABLED(CONFIG_UNICODE)
static int shmem_parse_opt_casefold(struct fs_context *fc, struct fs_parameter *param,
bool latest_version)
{
struct shmem_options *ctx = fc->fs_private;
unsigned int version = UTF8_LATEST;
struct unicode_map *encoding;
char *version_str = param->string + 5;
if (!latest_version) {
if (strncmp(param->string, "utf8-", 5))
return invalfc(fc, "Only UTF-8 encodings are supported "
"in the format: utf8-<version number>");
version = utf8_parse_version(version_str);
if (version < 0)
return invalfc(fc, "Invalid UTF-8 version: %s", version_str);
}
encoding = utf8_load(version);
if (IS_ERR(encoding)) {
return invalfc(fc, "Failed loading UTF-8 version: utf8-%u.%u.%u\n",
unicode_major(version), unicode_minor(version),
unicode_rev(version));
}
pr_info("tmpfs: Using encoding : utf8-%u.%u.%u\n",
unicode_major(version), unicode_minor(version), unicode_rev(version));
ctx->encoding = encoding;
return 0;
}
#else
static int shmem_parse_opt_casefold(struct fs_context *fc, struct fs_parameter *param,
bool latest_version)
{
return invalfc(fc, "tmpfs: Kernel not built with CONFIG_UNICODE\n");
}
#endif
static int shmem_parse_one(struct fs_context *fc, struct fs_parameter *param)
{
struct shmem_options *ctx = fc->fs_private;
@ -4384,6 +4459,17 @@ static int shmem_parse_one(struct fs_context *fc, struct fs_parameter *param)
"Group quota inode hardlimit too large.");
ctx->qlimits.grpquota_ihardlimit = size;
break;
case Opt_casefold_version:
return shmem_parse_opt_casefold(fc, param, false);
case Opt_casefold:
return shmem_parse_opt_casefold(fc, param, true);
case Opt_strict_encoding:
#if IS_ENABLED(CONFIG_UNICODE)
ctx->strict_encoding = true;
break;
#else
return invalfc(fc, "tmpfs: Kernel not built with CONFIG_UNICODE\n");
#endif
}
return 0;
@ -4613,6 +4699,11 @@ static void shmem_put_super(struct super_block *sb)
{
struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
#if IS_ENABLED(CONFIG_UNICODE)
if (sb->s_encoding)
utf8_unload(sb->s_encoding);
#endif
#ifdef CONFIG_TMPFS_QUOTA
shmem_disable_quotas(sb);
#endif
@ -4623,6 +4714,14 @@ static void shmem_put_super(struct super_block *sb)
sb->s_fs_info = NULL;
}
#if IS_ENABLED(CONFIG_UNICODE) && defined(CONFIG_TMPFS)
static const struct dentry_operations shmem_ci_dentry_ops = {
.d_hash = generic_ci_d_hash,
.d_compare = generic_ci_d_compare,
.d_delete = always_delete_dentry,
};
#endif
static int shmem_fill_super(struct super_block *sb, struct fs_context *fc)
{
struct shmem_options *ctx = fc->fs_private;
@ -4657,9 +4756,25 @@ static int shmem_fill_super(struct super_block *sb, struct fs_context *fc)
}
sb->s_export_op = &shmem_export_ops;
sb->s_flags |= SB_NOSEC | SB_I_VERSION;
#if IS_ENABLED(CONFIG_UNICODE)
if (!ctx->encoding && ctx->strict_encoding) {
pr_err("tmpfs: strict_encoding option without encoding is forbidden\n");
error = -EINVAL;
goto failed;
}
if (ctx->encoding) {
sb->s_encoding = ctx->encoding;
sb->s_d_op = &shmem_ci_dentry_ops;
if (ctx->strict_encoding)
sb->s_encoding_flags = SB_ENC_STRICT_MODE_FL;
}
#endif
#else
sb->s_flags |= SB_NOUSER;
#endif
#endif /* CONFIG_TMPFS */
sbinfo->max_blocks = ctx->blocks;
sbinfo->max_inodes = ctx->inodes;
sbinfo->free_ispace = sbinfo->max_inodes * BOGO_INODE_SIZE;
@ -4933,6 +5048,10 @@ int shmem_init_fs_context(struct fs_context *fc)
ctx->uid = current_fsuid();
ctx->gid = current_fsgid();
#if IS_ENABLED(CONFIG_UNICODE)
ctx->encoding = NULL;
#endif
fc->fs_private = ctx;
fc->ops = &shmem_fs_context_ops;
return 0;