Viewing: ext4-filename-encode.patch

From d0a722cb8fb886380e24e8261e8efca09a3262d6 Mon Sep 17 00:00:00 2001
From: Sebastien Buisson <sbuisson@ddn.com>
Date: Tue, 20 Dec 2022 15:40:52 +0100
Subject: [PATCH] LU-16374 ldiskfs: implement security.encdata xattr

security.encdata is a virtual xattr containing information related
to encrypted files. It is expressed as ASCII text with a "key: value"
format, and space as field separator. For instance:

   { encoding: base64url, size: 3012, enc_ctx: YWJjZGVmZ2hpamtsbW
   5vcHFyc3R1dnd4eXphYmNkZWZnaGlqa2xtbg, enc_name: ZmlsZXdpdGh2ZX
   J5bG9uZ25hbWVmaWxld2l0aHZlcnlsb25nbmFtZWZpbGV3aXRodmVyeWxvbmdu
   YW1lZmlsZXdpdGg }

'encoding' is the encoding method used for binary data, assume name
can be up to 255 chars.
'size' is the clear text file data length in bytes.
'enc_ctx' is encoded encryption context, 40 bytes for v2.
'enc_name' is encoded encrypted name, 256 bytes max.
So on overall, this xattr is at most 727 chars plus terminating '0'.

On get, the value of the security.encdata xattr is computed from
encrypted file's information.
On set, encrypted file's information is restored from xattr value.
The encrypted name is stored temporarily in a dedicated xattr
LDISKFS_XATTR_NAME_RAWENCNAME, that will be used to set correct name
at linkat.

Signed-off-by: Sebastien Buisson <sbuisson@ddn.com>
Tested-by: jenkins <devops@whamcloud.com>
Tested-by: Maloo <maloo@whamcloud.com>
Reviewed-by: Andreas Dilger <adilger@whamcloud.com>
Reviewed-by: Li Dongyang <dongyangli@ddn.com>
Reviewed-by: Oleg Drokin <green@whamcloud.com>
Change-Id: Ia318c39d403b1c448e71bcd5b29862d022d05d0a
Reviewed-on: https://review.whamcloud.com/49456
---
 fs/ext4/critical_encode.h | 170 ++++++++++++++++++++++++++++++++++++++
 fs/ext4/dir.c             |  33 +++++---
 fs/ext4/ialloc.c          |   1 +
 fs/ext4/namei.c           |  56 +++++++++----
 4 files changed, 235 insertions(+), 25 deletions(-)
 create mode 100644 fs/ext4/critical_encode.h

diff --git a/fs/ext4/critical_encode.h b/fs/ext4/critical_encode.h
new file mode 100644
index 00000000..f75aedab
--- /dev/null
+++ b/fs/ext4/critical_encode.h
@@ -0,0 +1,170 @@
+/*
+ *  critical_encode.h
+ *
+ *  Copyright (c) 2022 Whamcloud
+ */
+
+#ifndef _CRITICAL_ENCODE_H
+#define _CRITICAL_ENCODE_H
+
+#include <linux/ctype.h>
+
+/* Encoding/decoding routines inspired from yEnc principles.
+ * We just take care of a few critical characters:
+ * NULL, LF, CR, /, DEL and =.
+ * If such a char is found, it is replaced with '=' followed by
+ * the char value + 64.
+ * All other chars are left untouched.
+ * Efficiency of this encoding depends on the occurences of the
+ * critical chars, but statistically on binary data it can be much higher
+ * than base64 for instance.
+ */
+static inline int critical_encode(const u8 *src, int len, char *dst)
+{
+	u8 *p = (u8 *)src, *q = dst;
+
+	while (p - src < len) {
+		/* escape NULL, LF, CR, /, DEL and = */
+		if (unlikely(*p == 0x0 || *p == 0xA || *p == 0xD ||
+			     *p == '/' || *p == 0x7F || *p == '=')) {
+			*(q++) = '=';
+			*(q++) = *(p++) + 64;
+		} else {
+			*(q++) = *(p++);
+		}
+	}
+
+	return (char *)q - dst;
+}
+
+/* returns the number of chars encoding would produce */
+static inline int critical_chars(const u8 *src, int len)
+{
+	u8 *p = (u8 *)src;
+	int newlen = len;
+
+	while (p - src < len) {
+		/* NULL, LF, CR, /, DEL and = cost an additional '=' */
+		if (unlikely(*p == 0x0 || *p == 0xA || *p == 0xD ||
+			     *p == '/' || *p == 0x7F || *p == '='))
+			newlen++;
+		p++;
+	}
+
+	return newlen;
+}
+
+/* decoding routine - returns the number of chars in output */
+static inline int critical_decode(const u8 *src, int len, char *dst)
+{
+	u8 *p = (u8 *)src, *q = dst;
+
+	while (p - src < len) {
+		if (unlikely(*p == '=')) {
+			*(q++) = *(++p) - 64;
+			p++;
+		} else {
+			*(q++) = *(p++);
+		}
+	}
+
+	return (char *)q - dst;
+}
+
+#define fscrypt_get_encryption_info(inode) \
+	(unlikely(!IS_LUSTRE_MOUNT(inode->i_sb)) ? 0 : -EOPNOTSUPP)
+
+static inline int ext4_has_permitted_context(struct inode *parent,
+					struct inode *child)
+{
+	if (unlikely(!IS_LUSTRE_MOUNT(parent->i_sb)))
+		return 1;
+	return fscrypt_has_permitted_context(parent, child);
+}
+
+struct ext4_filename;
+
+static inline int ext4_prepare_readdir(struct inode *dir)
+{
+	if (unlikely(!IS_LUSTRE_MOUNT(dir->i_sb)))
+		return 0;
+	return fscrypt_prepare_readdir(dir);
+}
+
+static inline int ext4_fname_alloc_buffer(const struct inode *inode,
+					     u32 max_encrypted_len,
+					     struct fscrypt_str *crypto_str)
+{
+	crypto_str->name = kmalloc(max_encrypted_len + 1, GFP_NOFS);
+	if (!crypto_str->name)
+		return -ENOMEM;
+	crypto_str->len = max_encrypted_len;
+	return 0;
+}
+
+static inline void ext4_fname_free_buffer(struct fscrypt_str *crypto_str)
+{
+	if (!crypto_str)
+		return;
+	kfree(crypto_str->name);
+	crypto_str->name = NULL;
+}
+
+static inline int ext4_fname_disk_to_usr(struct inode *inode,
+					    u32 hash, u32 minor_hash,
+					    const struct fscrypt_str *iname,
+					    struct fscrypt_str *oname)
+{
+	int presented_len;
+
+	presented_len = critical_encode(iname->name, iname->len, oname->name);
+	if (presented_len > NAME_MAX) {
+		/* truncate at NAME_MAX,
+		 * or NAME_MAX-1 if name ends with '=' to avoid decoding issue
+		 */
+		presented_len = NAME_MAX;
+		if (oname->name[presented_len - 1] == '=')
+			presented_len--;
+		oname->len = presented_len;
+	}
+	oname->name[presented_len] = '\0';
+
+	return 0;
+}
+
+static inline int ext4_setup_filename(struct inode *dir,
+					 const struct qstr *iname,
+					 int lookup,
+					 struct ext4_filename *fname)
+{
+	fname->usr_fname = iname;
+
+	if (lookup && IS_ENCRYPTED(dir) &&
+	    unlikely(!IS_LUSTRE_MOUNT(dir->i_sb) &&
+		     strnchr(iname->name, iname->len, '='))) {
+		/* Only proceed to critical decode if
+		 * iname contains escape char '='.
+		 */
+		int len = iname->len;
+		char *buf;
+
+		buf = kmalloc(len, GFP_NOFS);
+		if (!buf)
+			return -ENOMEM;
+
+		len = critical_decode(iname->name, len, buf);
+		fname->disk_name.name = (unsigned char *)buf;
+		fname->disk_name.len = len;
+		return 0;
+	}
+
+	fname->disk_name.name = (unsigned char *) iname->name;
+	fname->disk_name.len = iname->len;
+
+#ifdef CONFIG_UNICODE
+	ext4_fname_setup_ci_filename(dir, iname, fname);
+#endif
+	return 0;
+}
+
+#endif /* _CRITICAL_ENCODE_H */
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index ae110d71..849b1e0e 100644
--- a/fs/ext4/dir.c
+++ b/fs/ext4/dir.c
@@ -29,6 +29,7 @@
 #include <linux/unicode.h>
 #include "ext4.h"
 #include "xattr.h"
+#include "critical_encode.h"
 
 static int ext4_dx_readdir(struct file *, struct dir_context *);
 
@@ -134,7 +135,7 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
 	struct buffer_head *bh = NULL;
 	struct fscrypt_str fstr = FSTR_INIT(NULL, 0);
 
-	err = fscrypt_prepare_readdir(inode);
+	err = ext4_prepare_readdir(inode);
 	if (err)
 		return err;
 
@@ -161,7 +162,8 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
 			return err;
 	}
 
-	if (IS_ENCRYPTED(inode)) {
+	/* disable decryption of filename, present only escaped name */
+	if (0 && IS_ENCRYPTED(inode)) {
 		err = fscrypt_fname_alloc_buffer(EXT4_NAME_LEN, &fstr);
 		if (err < 0)
 			return err;
@@ -275,24 +277,33 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
 					    get_dtype(sb, de->file_type)))
 						goto done;
 				} else {
-					int save_len = fstr.len;
 					struct fscrypt_str de_name =
 							FSTR_INIT(de->name,
 								de->name_len);
+					int presented_len;
 
 					/* Directory is encrypted */
-					err = fscrypt_fname_disk_to_usr(inode,
-						EXT4_DIRENT_HASH(de),
-						EXT4_DIRENT_MINOR_HASH(de),
-						&de_name, &fstr);
-					de_name = fstr;
-					fstr.len = save_len;
+					presented_len = critical_chars(de->name,
+								  de->name_len);
+					err = ext4_fname_alloc_buffer(inode,
+								  presented_len,
+								  &fstr);
 					if (err)
 						goto errout;
-					if (!dir_emit(ctx,
+
+					err = ext4_fname_disk_to_usr(inode,
+						0, 0, &de_name, &fstr);
+					de_name = fstr;
+					if (err) {
+						ext4_fname_free_buffer(&fstr);
+						goto errout;
+					}
+					err = dir_emit(ctx,
 					    de_name.name, de_name.len,
 					    le32_to_cpu(de->inode),
-					    get_dtype(sb, de->file_type)))
+					    get_dtype(sb, de->file_type));
+					ext4_fname_free_buffer(&fstr);
+					if (!err)
 						goto done;
 				}
 			}
diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index fc1f09fe..b12a4324 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -30,6 +30,7 @@
 #include "ext4_jbd2.h"
 #include "xattr.h"
 #include "acl.h"
+#include "critical_encode.h"
 
 #include <trace/events/ext4.h>
 
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 24b07d9d..1986ab5a 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -41,6 +41,7 @@
 
 #include "xattr.h"
 #include "acl.h"
+#include "critical_encode.h"
 
 #include <trace/events/ext4.h>
 /*
@@ -1441,7 +1442,7 @@ static int htree_dirblock_to_tree(struct file *dir_file,
 					   ext4_dir_rec_len(0,
 							   csum ? NULL : dir));
 	/* Check if the directory is encrypted */
-	if (IS_ENCRYPTED(dir)) {
+	if (0 && IS_ENCRYPTED(dir)) {
 		err = fscrypt_prepare_readdir(dir);
 		if (err < 0) {
 			brelse(bh);
@@ -1492,22 +1493,31 @@ static int htree_dirblock_to_tree(struct file *dir_file,
 				   hinfo->hash, hinfo->minor_hash, de,
 				   &tmp_str);
 		} else {
-			int save_len = fname_crypto_str.len;
 			struct fscrypt_str de_name = FSTR_INIT(de->name,
 								de->name_len);
+			int presented_len;
 
 			/* Directory is encrypted */
-			err = fscrypt_fname_disk_to_usr(dir, hinfo->hash,
+			presented_len = critical_chars(de->name, de->name_len);
+			err = ext4_fname_alloc_buffer(dir, presented_len,
+							 &fname_crypto_str);
+			if (err) {
+				count = err;
+				goto errout;
+			}
+
+			err = ext4_fname_disk_to_usr(dir, hinfo->hash,
 					hinfo->minor_hash, &de_name,
 					&fname_crypto_str);
 			if (err) {
+				ext4_fname_free_buffer(&fname_crypto_str);
 				count = err;
 				goto errout;
 			}
 			err = ext4_htree_store_dirent(dir_file,
 				   hinfo->hash, hinfo->minor_hash, de,
 					&fname_crypto_str);
-			fname_crypto_str.len = save_len;
+			ext4_fname_free_buffer(&fname_crypto_str);
 		}
 		if (err != 0) {
 			count = err;
@@ -1837,7 +1847,7 @@ int ext4_fname_setup_ci_filename(struct inode *dir, const struct qstr *iname,
  */
 static bool ext4_match(struct inode *parent,
 			      const struct ext4_filename *fname,
-			      struct ext4_dir_entry_2 *de)
+			      struct ext4_dir_entry_2 *de, int denamelen)
 {
 	struct fscrypt_name f;
 
@@ -1872,7 +1882,7 @@ static bool ext4_match(struct inode *parent,
 	}
 #endif
 
-	return fscrypt_match_name(&f, de->name, de->name_len);
+	return fscrypt_match_name(&f, de->name, denamelen);
 }
 
 /*
@@ -1883,16 +1893,30 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
 		    unsigned int offset, struct ext4_dir_entry_2 **res_dir)
 {
 	struct ext4_dir_entry_2 * de;
+	bool probablytrunc;
 	char * dlimit;
-	int de_len;
+	int de_len, denamelen;
 
 	de = (struct ext4_dir_entry_2 *)search_buf;
 	dlimit = search_buf + buf_size;
+	/* fname is probably truncated if it is the decoded representation of
+	 * an encrypted filename not aligned on a 32-byte boundary
+	 */
+	probablytrunc = !IS_LUSTRE_MOUNT(dir->i_sb) && IS_ENCRYPTED(dir) &&
+		fname->disk_name.len & 31;
 	while ((char *) de < dlimit - EXT4_BASE_DIR_LEN) {
 		/* this code is executed quadratically often */
 		/* do minimal checking `by hand' */
+		denamelen = de->name_len;
+		if (unlikely(probablytrunc) &&
+		    de->name_len > fname->disk_name.len)
+			/* Adjust name len to look for a partial match.
+			 * Since it is binary encrypted names, there
+			 * should not be any collision between names.
+			 */
+			denamelen = fname->disk_name.len;
 		if (de->name + de->name_len <= dlimit &&
-		    ext4_match(dir, fname, de)) {
+		    ext4_match(dir, fname, de, denamelen)) {
 			/* found a match - just to be sure, do
 			 * a full check */
 			if (ext4_check_dir_entry(dir, NULL, de, bh, search_buf,
@@ -2093,7 +2117,7 @@ struct buffer_head *ext4_find_entry_locked(struct inode *dir,
 	struct ext4_filename fname;
 	struct buffer_head *bh;
 
-	err = ext4_fname_setup_filename(dir, d_name, 1, &fname);
+	err = ext4_setup_filename(dir, d_name, 1, &fname);
 	if (err == -ENOENT)
 		return NULL;
 	if (err)
@@ -2101,7 +2125,9 @@ struct buffer_head *ext4_find_entry_locked(struct inode *dir,
 
 	bh = __ext4_find_entry(dir, &fname, res_dir, inlined, lck);
 
-	ext4_fname_free_filename(&fname);
+	if (fname.disk_name.name != d_name->name)
+		kfree(fname.disk_name.name);
+
 	return bh;
 }
 
@@ -2115,7 +2141,7 @@ static struct buffer_head *ext4_lookup_entry(struct inode *dir,
 	struct ext4_filename fname;
 	struct buffer_head *bh;
 
-	err = ext4_fname_prepare_lookup(dir, dentry, &fname);
+	err = ext4_setup_filename(dir, &dentry->d_name, 1, &fname);
 	if (err == -ENOENT)
 		return NULL;
 	if (err)
@@ -2123,7 +2149,9 @@ static struct buffer_head *ext4_lookup_entry(struct inode *dir,
 
 	bh = __ext4_find_entry(dir, &fname, res_dir, NULL, NULL);
 
-	ext4_fname_free_filename(&fname);
+	if (fname.disk_name.name != dentry->d_name.name)
+		kfree(fname.disk_name.name);
+
 	return bh;
 }
 
@@ -2215,7 +2243,7 @@ static struct dentry *ext4_lookup(struct inode *dir, struct dentry *dentry, unsi
 		}
 		if (!IS_ERR(inode) && IS_ENCRYPTED(dir) &&
 		    (S_ISDIR(inode->i_mode) || S_ISLNK(inode->i_mode)) &&
-		    !fscrypt_has_permitted_context(dir, inode)) {
+		    !ext4_has_permitted_context(dir, inode)) {
 			ext4_warning(inode->i_sb,
 				     "Inconsistent encryption contexts: %lu/%lu",
 				     dir->i_ino, inode->i_ino);
@@ -2508,7 +2536,7 @@ int ext4_find_dest_de(struct inode *dir, struct inode *inode,
 		if (ext4_check_dir_entry(dir, NULL, de, bh,
 					 buf, buf_size, offset))
 			return -EFSCORRUPTED;
-		if (ext4_match(dir, fname, de))
+		if (ext4_match(dir, fname, de, de->name_len))
 			return -EEXIST;
 		nlen = EXT4_DIR_ENTRY_LEN(de, dir);
 		rlen = ext4_rec_len_from_disk(de->rec_len, buf_size);
-- 
2.34.1