Skip to content

Commit

Permalink
exfat: allow access to paths with trailing dots
Browse files Browse the repository at this point in the history
 exfat currently unconditionally strips trailing
 periods '.' when performing path lookup, but allows them in the filenames
 during file creation. This is done intentionally, loosely following
 Windows behaviour and specifications which state:

  #exFAT
  The concatenated file name has the same set of illegal characters as
  other FAT-based file systems (see Table 31).

  #FAT
  ...
  Leading and trailing spaces in a long name are ignored.
  Leading and embedded periods are allowed in a name and are stored in
  the long name. Trailing periods are ignored.

Note: Leading and trailing space ' ' characters are currently retained
by Linux kernel exfat, in conflict with the above specification.
On Windows 10, trailing and leading space ' ' characters are stripped
from the filenames
.
Some implementations, such as fuse-exfat, don't perform path trailer
removal. When mounting images which contain trailing-dot paths, these
paths are unreachable, e.g.:

  + mount.exfat-fuse /dev/zram0 /mnt/test/
  FUSE exfat 1.3.0
  + cd /mnt/test/
  + touch fuse_created_dots... '  fuse_created_spaces  '
  + ls -l
  total 0
  -rwxrwxrwx 1 root 0 0 Aug 18 09:45 '  fuse_created_spaces  '
  -rwxrwxrwx 1 root 0 0 Aug 18 09:45  fuse_created_dots...
  + cd /
  + umount /mnt/test/
  + mount -t exfat /dev/zram0 /mnt/test
  + cd /mnt/test
  + ls -l
  ls: cannot access 'fuse_created_dots...': No such file or directory
  total 0
  -rwxr-xr-x 1 root 0 0 Aug 18 09:45 '  fuse_created_spaces  '
  -????????? ? ?    ? ?            ?  fuse_created_dots...
  + touch kexfat_created_dots... '  kexfat_created_spaces  '
  + ls -l
  ls: cannot access 'fuse_created_dots...': No such file or directory
  total 0
  -rwxr-xr-x 1 root 0 0 Aug 18 09:45 '  fuse_created_spaces  '
  -rwxr-xr-x 1 root 0 0 Aug 18 09:45 '  kexfat_created_spaces  '
  -????????? ? ?    ? ?            ?  fuse_created_dots...
  -rwxr-xr-x 1 root 0 0 Aug 18 09:45  kexfat_created_dots
  + cd /
  + umount /mnt/test/

This commit adds "keep_last_dots" mount option that controls whether or
not trailing periods '.' are stripped
from path components during file lookup or file creation.
This mount option can be used to access
paths with trailing periods and disallow creating files with names with
trailing periods. E.g. continuing from the previous example:

  + mount -t exfat -o keep_last_dots /dev/zram0 /mnt/test
  + cd /mnt/test
  + ls -l
  total 0
  -rwxr-xr-x 1 root 0 0 Aug 18 10:32 '  fuse_created_spaces  '
  -rwxr-xr-x 1 root 0 0 Aug 18 10:32 '  kexfat_created_spaces  '
  -rwxr-xr-x 1 root 0 0 Aug 18 10:32  fuse_created_dots...
  -rwxr-xr-x 1 root 0 0 Aug 18 10:32  kexfat_created_dots

  + echo > kexfat_created_dots_again...
  sh: kexfat_created_dots_again...: Invalid argument

Link: https://bugzilla.suse.com/show_bug.cgi?id=1188964
Link: https://lore.kernel.org/linux-fsdevel/003b01d755e4$31fb0d80$95f12880$
@samsung.com/
Link: https://docs.microsoft.com/en-us/windows/win32/fileio/exfat-specification
Suggested-by: Takashi Iwai <[email protected]>
Signed-off-by: Vasant Karasulli <[email protected]>
Co-developed-by: David Disseldorp <[email protected]>
Signed-off-by: David Disseldorp <[email protected]>
Signed-off-by: Namjae Jeon <[email protected]>
  • Loading branch information
vsntk18 authored and namjaejeon committed Mar 18, 2022
1 parent 1c014cb commit 531f58f
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 15 deletions.
3 changes: 2 additions & 1 deletion exfat_fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ struct exfat_mount_options {
/* on error: continue, panic, remount-ro */
enum exfat_error_mode errors;
unsigned utf8:1, /* Use of UTF-8 character set */
discard:1; /* Issue discard requests on deletions */
discard:1, /* Issue discard requests on deletions */
keep_last_dots:1; /* Keep trailing periods in paths */
int time_offset; /* Offset of timestamps from UTC (in minutes) */
};

Expand Down
50 changes: 36 additions & 14 deletions namei.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,14 @@ static int exfat_d_revalidate(struct dentry *dentry, unsigned int flags)
return ret;
}

/* returns the length of a struct qstr, ignoring trailing dots */
static unsigned int exfat_striptail_len(unsigned int len, const char *name)
/* returns the length of a struct qstr, ignoring trailing dots if necessary */
static unsigned int exfat_striptail_len(unsigned int len, const char *name,
bool keep_last_dots)
{
while (len && name[len - 1] == '.')
len--;
if (!keep_last_dots) {
while (len && name[len - 1] == '.')
len--;
}
return len;
}

Expand All @@ -91,7 +94,8 @@ static int exfat_d_hash(const struct dentry *dentry, struct qstr *qstr)
struct super_block *sb = dentry->d_sb;
struct nls_table *t = EXFAT_SB(sb)->nls_io;
const unsigned char *name = qstr->name;
unsigned int len = exfat_striptail_len(qstr->len, qstr->name);
unsigned int len = exfat_striptail_len(qstr->len, qstr->name,
EXFAT_SB(sb)->options.keep_last_dots);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 8, 0)
unsigned long hash = init_name_hash(dentry);
#else
Expand Down Expand Up @@ -121,8 +125,10 @@ static int exfat_d_cmp(const struct dentry *parent, const struct dentry *dentry,
{
struct super_block *sb = dentry->d_sb;
struct nls_table *t = EXFAT_SB(sb)->nls_io;
unsigned int alen = exfat_striptail_len(name->len, name->name);
unsigned int blen = exfat_striptail_len(len, str);
unsigned int alen = exfat_striptail_len(name->len, name->name,
EXFAT_SB(sb)->options.keep_last_dots);
unsigned int blen = exfat_striptail_len(len, str,
EXFAT_SB(sb)->options.keep_last_dots);
wchar_t c1, c2;
int charlen, i;

Expand Down Expand Up @@ -153,7 +159,8 @@ static int exfat_utf8_d_hash(const struct dentry *dentry, struct qstr *qstr)
{
struct super_block *sb = dentry->d_sb;
const unsigned char *name = qstr->name;
unsigned int len = exfat_striptail_len(qstr->len, qstr->name);
unsigned int len = exfat_striptail_len(qstr->len, qstr->name,
EXFAT_SB(sb)->options.keep_last_dots);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 8, 0)
unsigned long hash = init_name_hash(dentry);
#else
Expand Down Expand Up @@ -188,8 +195,11 @@ static int exfat_utf8_d_cmp(const struct dentry *parent,
#endif
{
struct super_block *sb = dentry->d_sb;
unsigned int alen = exfat_striptail_len(name->len, name->name);
unsigned int blen = exfat_striptail_len(len, str);
unsigned int alen = exfat_striptail_len(name->len, name->name,
EXFAT_SB(sb)->options.keep_last_dots);
unsigned int blen = exfat_striptail_len(len, str,
EXFAT_SB(sb)->options.keep_last_dots);

unicode_t u_a, u_b;
int charlen, i;

Expand Down Expand Up @@ -443,13 +453,25 @@ static int __exfat_resolve_path(struct inode *inode, const unsigned char *path,
struct super_block *sb = inode->i_sb;
struct exfat_sb_info *sbi = EXFAT_SB(sb);
struct exfat_inode_info *ei = EXFAT_I(inode);
int pathlen = strlen(path);

/* strip all trailing periods */
namelen = exfat_striptail_len(strlen(path), path);
/*
* get the length of the pathname excluding
* trailing periods, if any.
*/
namelen = exfat_striptail_len(pathlen, path, false);
if (EXFAT_SB(sb)->options.keep_last_dots) {
/*
* Do not allow the creation of files with names
* ending with period(s).
*/
if (!lookup && (namelen < pathlen))
return -EINVAL;
namelen = pathlen;
}
if (!namelen)
return -ENOENT;

if (strlen(path) > (MAX_NAME_LENGTH * MAX_CHARSET_SIZE))
if (pathlen > (MAX_NAME_LENGTH * MAX_CHARSET_SIZE))
return -ENAMETOOLONG;

/*
Expand Down
7 changes: 7 additions & 0 deletions super.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ static int exfat_show_options(struct seq_file *m, struct dentry *root)
seq_puts(m, ",errors=remount-ro");
if (opts->discard)
seq_puts(m, ",discard");
if (opts->keep_last_dots)
seq_puts(m, ",keep_last_dots");
if (opts->time_offset)
seq_printf(m, ",time_offset=%d", opts->time_offset);
return 0;
Expand Down Expand Up @@ -269,6 +271,7 @@ enum {
Opt_charset,
Opt_errors,
Opt_discard,
Opt_keep_last_dots,
Opt_time_offset,

/* Deprecated options */
Expand Down Expand Up @@ -312,6 +315,7 @@ static const struct fs_parameter_spec exfat_param_specs[] = {
fsparam_enum("errors", Opt_errors),
#endif
fsparam_flag("discard", Opt_discard),
fsparam_flag("keep_last_dots", Opt_keep_last_dots),
fsparam_s32("time_offset", Opt_time_offset),
__fsparam(NULL, "utf8", Opt_utf8, fs_param_deprecated,
NULL),
Expand Down Expand Up @@ -378,6 +382,9 @@ static int exfat_parse_param(struct fs_context *fc, struct fs_parameter *param)
case Opt_discard:
opts->discard = 1;
break;
case Opt_keep_last_dots:
opts->keep_last_dots = 1;
break;
case Opt_time_offset:
/*
* Make the limit 24 just in case someone invents something
Expand Down

0 comments on commit 531f58f

Please sign in to comment.