Skip to contentSkip to conversation

mountd: add support for case-insensitive file names.

PATCH J. Bruce Fields 2 participants 2 messages Jun 12, 2014 – Jun 17, 2014 1 message not in the archive

Subsystems (2)
lore
Share & tools
lore

A message that is not in the archive (sent off-list or not yet received): [email protected]

#1 lore raw
Quoted 17 lines · NeilBrown

Case insensitive filesystems support textually distinct names for the same directory. i.e. you can access it with a name other than the canonical name.
For example if you
mkdir /mnt/export

then add /mnt/EXPORT to /etc/exports, and on a client
mount server:/mnt/EXPORT /import

then the mount will work, but if the kernel on the server needs to refresh the export information, it will ask about "/mnt/export", which is not listed in /etc/exports and so will fail.

To fix this we need mountd to perform case-insensitive name comparisons, but only when the filesystem would, and in exactly the same way that the filesystem would.

Quoted 7 lines

So, when comparing paths for equality first try some simple heuristics which will not be fooled by case and then ask the kernel if they are the same.

By preference we use name_to_handle_at() as it reports the mntid which can distinguish between bind mounts. If that is not available, use lstat() and compare rdev and ino.

ACK to the patch.

Quoted 9 lines

---
This was discussed a little while ago under the subject
Should exportfs/mountd cope with case-insensitive directory names.

I've added name_to_handle_at support if it is available to get a more precise result.

Quoted 8 lines

I've tested by exporting a subdirectory of a VFAT filesystem using a different name than the one the directory was created with.
This works with old code until you

date +%s > /proc/net/rpc/nfsd.export/flush

(or wait 15 minutes) at which point you get a stale file handle. With the patch, you don't get the stale file handle.

I'd stick leave the test case in the changelog, though.

Actually, ideally we'd have it in code.

The exports logic is pretty complicated and I've got no regular regression testing for it.

--b.

Quoted 121 lines

Thanks,
NeilBrown

configure.ac +1 -1
index 7b93de6386db..408f4c85bd98 100644
@@ -411,7 +411,7 @@ AC_CHECK_FUNCS([alarm atexit dup2 fdatasync ftruncate getcwd \
                getnameinfo getrpcbyname getifaddrs \
                gettimeofday hasmntopt inet_ntoa innetgr memset mkdir pathconf \
                ppoll realpath rmdir select socket strcasecmp strchr strdup \
-               strerror strrchr strtol strtoul sigprocmask])
+               strerror strrchr strtol strtoul sigprocmask name_to_handle_at])
 
 
 dnl *************************************************************
utils/mountd/cache.c +76 -2
index 9a1bb2767ac2..6dad257e03ce 100644
@@ -377,6 +377,80 @@ static char *next_mnt(void **v, char *p)
 	return me->mnt_dir;
 }
 
+/* same_path() check is two paths refer to the same directory.
+ * We don't rely on 'strcmp()' as some filesystems support case-insensitive
+ * names and we might have two different names for the one directory.
+ * Theoretically the lengths of the names could be different, but the
+ * number of components must be the same.
+ * So if the paths have the same number of components (but aren't identical)
+ * we ask the kernel if they are the same thing.
+ * By preference we use name_to_handle_at(), as the mntid it returns
+ * will distinguish between bind-mount points.  If that isn't available
+ * we fall back on lstat, which is usually good enough.
+ */
+static inline int count_slashes(char *p)
+{
+	int cnt = 0;
+	while (*p)
+		if (*p++ == '/')
+			cnt++;
+	return cnt;
+}
+
+static int same_path(char *child, char *parent, int len)
+{
+	static char p[PATH_MAX];
+	if (len <= 0)
+		len = strlen(child);
+	strncpy(p, child, len);
+	p[len] = 0;
+	if (strcmp(p, parent) == 0)
+		return 1;
+
+	/* If number of '/' are different, they must be different */
+	if (count_slashes(p) != count_slashes(parent))
+		return 0;
+
+#if HAVE_NAME_TO_HANDLE_AT
+	struct {
+		struct file_handle fh;
+		unsigned char handle[128];
+	} fchild, fparent;
+	int mnt_child, mnt_parent;
+	fchild.fh.handle_bytes = 128;
+	fparent.fh.handle_bytes = 128;
+	if (name_to_handle_at(AT_FDCWD, p, &fchild.fh, &mnt_child, 0) != 0) {
+		if (errno == ENOSYS)
+			goto fallback;
+		return 0;
+	}
+	if (name_to_handle_at(AT_FDCWD, parent, &fparent.fh, &mnt_parent, 0) != 0)
+		return 0;
+	if (mnt_child != mnt_parent ||
+	    fchild.fh.handle_bytes != fparent.fh.handle_bytes ||
+	    fchild.fh.handle_type != fparent.fh.handle_type ||
+	    memcmp(fchild.handle, fparent.handle,
+		   fchild.fh.handle_bytes) != 0)
+		return 0;
+	return 1;
+fallback:;
+#endif
+	/* This is nearly good enough.  However if a directory is
+	 * bind-mounted in two places and both are exported, it
+	 * could give a false positive
+	 */
+	struct stat sc, sp;
+	if (lstat(p, &sc) != 0)
+		return 0;
+	if (lstat(parent, &sp) != 0)
+		return 0;
+	if (sc.st_dev != sp.st_dev)
+		return 0;
+	if (sc.st_ino != sp.st_ino)
+		return 0;
+	return 1;
+}
+
 static int is_subdirectory(char *child, char *parent)
 {
 	/* Check is child is strictly a subdirectory of
@@ -387,7 +461,7 @@ static int is_subdirectory(char *child, char *parent)
 	if (strcmp(parent, "/") == 0 && child[1] != 0)
 		return 1;
 
-	return (strncmp(child, parent, l) == 0 && child[l] == '/');
+	return (same_path(child, parent, l) && child[l] == '/');
 }
 
 static int path_matches(nfs_export *exp, char *path)
@@ -396,7 +470,7 @@ static int path_matches(nfs_export *exp, char *path)
 	 * exact match, or does the export have CROSSMOUNT, and path
 	 * is a descendant?
 	 */
-	return strcmp(path, exp->m_export.e_path) == 0
+	return same_path(path, exp->m_export.e_path, 0)
 		|| ((exp->m_export.e_flags & NFSEXP_CROSSMOUNT)
 		    && is_subdirectory(path, exp->m_export.e_path));
 }
#2 lore raw
In reply to a message not in the archive
Quoted 23 lines · NeilBrown

Case insensitive filesystems support textually distinct names for the same directory. i.e. you can access it with a name other than the canonical name.
For example if you
mkdir /mnt/export

then add /mnt/EXPORT to /etc/exports, and on a client
mount server:/mnt/EXPORT /import

then the mount will work, but if the kernel on the server needs to refresh the export information, it will ask about "/mnt/export", which is not listed in /etc/exports and so will fail.

To fix this we need mountd to perform case-insensitive name comparisons, but only when the filesystem would, and in exactly the same way that the filesystem would.

So, when comparing paths for equality first try some simple heuristics which will not be fooled by case and then ask the kernel if they are the same.

Quoted 5 lines

By preference we use name_to_handle_at() as it reports the mntid which can distinguish between bind mounts. If that is not available, use lstat() and compare rdev and ino.

Committed...

steved.

Quoted 138 lines

---
This was discussed a little while ago under the subject
Should exportfs/mountd cope with case-insensitive directory names.

I've added name_to_handle_at support if it is available to get a more precise result.

I've tested by exporting a subdirectory of a VFAT filesystem using a different name than the one the directory was created with.
This works with old code until you

date +%s > /proc/net/rpc/nfsd.export/flush

(or wait 15 minutes) at which point you get a stale file handle. With the patch, you don't get the stale file handle.

Thanks,
NeilBrown

configure.ac +1 -1
index 7b93de6386db..408f4c85bd98 100644
@@ -411,7 +411,7 @@ AC_CHECK_FUNCS([alarm atexit dup2 fdatasync ftruncate getcwd \
                getnameinfo getrpcbyname getifaddrs \
                gettimeofday hasmntopt inet_ntoa innetgr memset mkdir pathconf \
                ppoll realpath rmdir select socket strcasecmp strchr strdup \
-               strerror strrchr strtol strtoul sigprocmask])
+               strerror strrchr strtol strtoul sigprocmask name_to_handle_at])
 
 
 dnl *************************************************************
utils/mountd/cache.c +76 -2
index 9a1bb2767ac2..6dad257e03ce 100644
@@ -377,6 +377,80 @@ static char *next_mnt(void **v, char *p)
 	return me->mnt_dir;
 }
 
+/* same_path() check is two paths refer to the same directory.
+ * We don't rely on 'strcmp()' as some filesystems support case-insensitive
+ * names and we might have two different names for the one directory.
+ * Theoretically the lengths of the names could be different, but the
+ * number of components must be the same.
+ * So if the paths have the same number of components (but aren't identical)
+ * we ask the kernel if they are the same thing.
+ * By preference we use name_to_handle_at(), as the mntid it returns
+ * will distinguish between bind-mount points.  If that isn't available
+ * we fall back on lstat, which is usually good enough.
+ */
+static inline int count_slashes(char *p)
+{
+	int cnt = 0;
+	while (*p)
+		if (*p++ == '/')
+			cnt++;
+	return cnt;
+}
+
+static int same_path(char *child, char *parent, int len)
+{
+	static char p[PATH_MAX];
+	if (len <= 0)
+		len = strlen(child);
+	strncpy(p, child, len);
+	p[len] = 0;
+	if (strcmp(p, parent) == 0)
+		return 1;
+
+	/* If number of '/' are different, they must be different */
+	if (count_slashes(p) != count_slashes(parent))
+		return 0;
+
+#if HAVE_NAME_TO_HANDLE_AT
+	struct {
+		struct file_handle fh;
+		unsigned char handle[128];
+	} fchild, fparent;
+	int mnt_child, mnt_parent;
+	fchild.fh.handle_bytes = 128;
+	fparent.fh.handle_bytes = 128;
+	if (name_to_handle_at(AT_FDCWD, p, &fchild.fh, &mnt_child, 0) != 0) {
+		if (errno == ENOSYS)
+			goto fallback;
+		return 0;
+	}
+	if (name_to_handle_at(AT_FDCWD, parent, &fparent.fh, &mnt_parent, 0) != 0)
+		return 0;
+	if (mnt_child != mnt_parent ||
+	    fchild.fh.handle_bytes != fparent.fh.handle_bytes ||
+	    fchild.fh.handle_type != fparent.fh.handle_type ||
+	    memcmp(fchild.handle, fparent.handle,
+		   fchild.fh.handle_bytes) != 0)
+		return 0;
+	return 1;
+fallback:;
+#endif
+	/* This is nearly good enough.  However if a directory is
+	 * bind-mounted in two places and both are exported, it
+	 * could give a false positive
+	 */
+	struct stat sc, sp;
+	if (lstat(p, &sc) != 0)
+		return 0;
+	if (lstat(parent, &sp) != 0)
+		return 0;
+	if (sc.st_dev != sp.st_dev)
+		return 0;
+	if (sc.st_ino != sp.st_ino)
+		return 0;
+	return 1;
+}
+
 static int is_subdirectory(char *child, char *parent)
 {
 	/* Check is child is strictly a subdirectory of
@@ -387,7 +461,7 @@ static int is_subdirectory(char *child, char *parent)
 	if (strcmp(parent, "/") == 0 && child[1] != 0)
 		return 1;
 
-	return (strncmp(child, parent, l) == 0 && child[l] == '/');
+	return (same_path(child, parent, l) && child[l] == '/');
 }
 
 static int path_matches(nfs_export *exp, char *path)
@@ -396,7 +470,7 @@ static int path_matches(nfs_export *exp, char *path)
 	 * exact match, or does the export have CROSSMOUNT, and path
 	 * is a descendant?
 	 */
-	return strcmp(path, exp->m_export.e_path) == 0
+	return same_path(path, exp->m_export.e_path, 0)
 		|| ((exp->m_export.e_flags & NFSEXP_CROSSMOUNT)
 		    && is_subdirectory(path, exp->m_export.e_path));
 }