diff -wur /dev/null b/fs/ext4/critical_encode.h --- /dev/null +++ b/fs/ext4/critical_encode.h @@ -0,0 +1,166 @@ +/* + * critical_encode.h + * + * Copyright (c) 2022 Whamcloud + */ + +#ifndef _CRITICAL_ENCODE_H +#define _CRITICAL_ENCODE_H + +#include + +/* 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); +} + +static inline int ext4_prepare_lookup(struct inode *dir, + struct dentry *dentry, + unsigned int flags) +{ + if (unlikely(!IS_LUSTRE_MOUNT(dir->i_sb))) + return 0; + return fscrypt_prepare_lookup(dir, dentry, flags); +} + +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; + return 0; +} + +#endif /* _CRITICAL_ENCODE_H */ diff -wur a/fs/ext4/dir.c b/fs/ext4/dir.c --- a/fs/ext4/dir.c +++ b/fs/ext4/dir.c @@ -28,6 +28,7 @@ #include #include "ext4.h" #include "xattr.h" +#include "critical_encode.h" static int ext4_dx_readdir(struct file *, struct dir_context *); @@ -144,7 +145,8 @@ static int ext4_readdir(struct file * return err; } - if (IS_ENCRYPTED(inode)) { + /* disable decryption of filename, present only escaped name */ + if (0 && IS_ENCRYPTED(inode)) { err = fscrypt_fname_alloc_buffer(inode, EXT4_NAME_LEN, &fstr); if (err < 0) return err; @@ -258,22 +259,33 @@ static int ext4_readdir(struct file * 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, + presented_len = critical_chars(de->name, + de->name_len); + err = ext4_fname_alloc_buffer(inode, + presented_len, + &fstr); + if (err) + goto errout; + + err = ext4_fname_disk_to_usr(inode, 0, 0, &de_name, &fstr); de_name = fstr; - fstr.len = save_len; - if (err) + if (err) { + ext4_fname_free_buffer(&fstr); goto errout; - if (!dir_emit(ctx, + } + 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 -wur a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c --- 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 diff -wur a/fs/ext4/namei.c b/fs/ext4/namei.c --- a/fs/ext4/namei.c +++ b/fs/ext4/namei.c @@ -40,6 +40,7 @@ #include "xattr.h" #include "acl.h" +#include "critical_encode.h" #include /* @@ -1368,22 +1369,31 @@ static int htree_dirblock_to_tree(struct 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; @@ -1614,7 +1614,7 @@ static void dx_insert_block(struct dx_fr * Return: %true if the directory entry matches, otherwise %false. */ static inline bool ext4_match(const struct ext4_filename *fname, - const struct ext4_dir_entry_2 *de) + const struct ext4_dir_entry_2 *de, int denamelen) { struct fscrypt_name f; @@ -1626,7 +1626,7 @@ static inline bool ext4_match(const s #ifdef CONFIG_EXT4_FS_ENCRYPTION f.crypto_buf = fname->crypto_buf; #endif - return fscrypt_match_name(&f, de->name, de->name_len); + return fscrypt_match_name(&f, de->name, denamelen); } /* @@ -1637,16 +1637,30 @@ int ext4_search_dir(struct buffer_hea 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) { /* 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 ((char *) de + de->name_len <= dlimit && - ext4_match(fname, de)) { + ext4_match(fname, de, denamelen)) { /* found a match - just to be sure, do * a full check */ if (ext4_check_dir_entry(dir, NULL, de, bh, bh->b_data, @@ -1707,7 +1717,7 @@ struct buffer_head *__ext4_find_entry if (namelen > EXT4_NAME_LEN) return NULL; - retval = ext4_fname_setup_filename(dir, d_name, 1, &fname); + retval = ext4_setup_filename(dir, d_name, 1, &fname); if (retval == -ENOENT) return NULL; if (retval) @@ -1834,7 +1844,8 @@ cleanup_and_exit: /* Clean up the read-ahead blocks */ for (; ra_ptr < ra_max; ra_ptr++) brelse(bh_use[ra_ptr]); - ext4_fname_free_filename(&fname); + if (fname.disk_name.name != d_name->name) + kfree(fname.disk_name.name); return ret; } EXPORT_SYMBOL(__ext4_find_entry); @@ -1900,7 +1911,7 @@ static struct dentry *ext4_lookup(str struct buffer_head *bh; int err; - err = fscrypt_prepare_lookup(dir, dentry, flags); + err = ext4_prepare_lookup(dir, dentry, flags); if (err) return ERR_PTR(err); @@ -1957,7 +1957,7 @@ static struct dentry *ext4_lookup(struct } 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); @@ -2206,7 +2221,7 @@ int ext4_find_dest_de(struct inode *d if (ext4_check_dir_entry(dir, NULL, de, bh, buf, buf_size, offset)) return -EFSCORRUPTED; - if (ext4_match(fname, de)) + if (ext4_match(fname, de, de->name_len)) return -EEXIST; nlen = EXT4_DIR_ENTRY_LEN(de); rlen = ext4_rec_len_from_disk(de->rec_len, buf_size);