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 *filp, void *dirent, filldir_t filldir); @@ -243,12 +244,41 @@ revalidate: * during the copy operation. */ u64 version = filp->f_version; + int presented_len = de->name_len; + char *buf = de->name; - error = filldir(dirent, de->name, - de->name_len, + if (unlikely(!IS_LUSTRE_MOUNT(inode->i_sb)) && + EXT4_I(inode)->i_flags & EXT4_ENCRYPT_FL) + presented_len = critical_chars(de->name, + de->name_len); + if (unlikely(de->name_len != presented_len)) { + buf = kmalloc(presented_len + 1, + GFP_NOFS); + if (!buf) { + error = -ENOMEM; + break; + } + critical_encode(de->name, + de->name_len, buf); + 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 (buf[NAME_MAX - 1] == '=') + presented_len--; + } + buf[presented_len] = '\0'; + } + + error = filldir(dirent, buf, + presented_len, filp->f_pos, le32_to_cpu(de->inode), get_dtype(sb, de->file_type)); + if (unlikely(de->name_len != presented_len)) + kfree(buf); if (error) break; if (version != filp->f_version) @@ -516,10 +542,38 @@ static int call_filldir(struct file *fil } curr_pos = hash2pos(filp, fname->hash, fname->minor_hash); while (fname) { - error = filldir(dirent, fname->name, - fname->name_len, curr_pos, + int presented_len = fname->name_len; + char *buf = fname->name; + + if (unlikely(!IS_LUSTRE_MOUNT(inode->i_sb)) && + EXT4_I(inode)->i_flags & EXT4_ENCRYPT_FL) + presented_len = critical_chars(fname->name, + fname->name_len); + if (unlikely(fname->name_len != presented_len)) { + buf = kmalloc(presented_len + 1, GFP_NOFS); + if (!buf) { + filp->f_pos = curr_pos; + info->extra_fname = fname; + return -ENOMEM; + } + critical_encode(fname->name, + fname->name_len, buf); + 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 (buf[presented_len - 1] == '=') + presented_len--; + } + buf[presented_len] = '\0'; + } + error = filldir(dirent, buf, + presented_len, curr_pos, fname->inode, get_dtype(sb, fname->file_type)); + if (unlikely(fname->name_len != presented_len)) + kfree(buf); if (error) { filp->f_pos = curr_pos; info->extra_fname = fname; diff -wur /dev/null b/fs/ext4/critical_encode.h --- /dev/null +++ b/fs/ext4/critical_encode.h @@ -0,0 +1,103 @@ +/* + * 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; +} + +static inline int ext4_setup_filename(struct inode *dir, + const struct qstr *iname, + int lookup, + struct qstr *qstr) +{ + if (lookup && unlikely(!IS_LUSTRE_MOUNT(dir->i_sb)) && + EXT4_I(dir)->i_flags & EXT4_ENCRYPT_FL && + 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); + qstr->name = buf; + qstr->len = len; + } else { + qstr->name = iname->name; + qstr->len = iname->len; + } + + return 0; +} + +#endif /* _CRITICAL_ENCODE_H */ diff -wur a/fs/ext4/namei.c b/fs/ext4/namei.c --- a/fs/ext4/namei.c +++ b/fs/ext4/namei.c @@ -39,6 +39,7 @@ #include "xattr.h" #include "acl.h" +#include "critical_encode.h" #include /* @@ -1494,9 +1494,9 @@ static void dx_insert_block(struct dx_fr * `de != NULL' is guaranteed by caller. */ static inline int ext4_match (int len, const char * const name, - struct ext4_dir_entry_2 * de) + struct ext4_dir_entry_2 * de, int denamelen) { - if (len != de->name_len) + if (len != denamelen) return 0; if (!de->inode) return 0; @@ -1516,18 +1516,30 @@ int search_dir(struct buffer_head *bh, { struct ext4_dir_entry_2 * de; char * dlimit; - int de_len; + int de_len, denamelen; const char *name = d_name->name; int namelen = d_name->len; + bool probablytrunc; 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) && + EXT4_I(dir)->i_flags & EXT4_ENCRYPT_FL && namelen & 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 > namelen) + /* Adjust name len to look for a partial match. + * Since it is binary encrypted names, there + * should not be any collision between names. + */ + denamelen = namelen; if ((char *) de + namelen <= dlimit && - ext4_match (namelen, name, de)) { + ext4_match(namelen, name, 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, bh->b_size, offset)) @@ -1588,8 +1589,9 @@ struct buffer_head *__ext4_find_entry buffer */ int num = 0; ext4_lblk_t nblocks; int i, err = 0; int namelen; + struct qstr qstr = QSTR_INIT(NULL, 0); *res_dir = NULL; sb = dir->i_sb; @@ -1597,6 +1600,11 @@ struct buffer_head *__ext4_find_entry if (namelen > EXT4_NAME_LEN) return NULL; + err = ext4_setup_filename(dir, d_name, 1, &qstr); + if (err) + return ERR_PTR(err); + d_name = &qstr; + if (ext4_has_inline_data(dir)) { int has_inline_data = 1; ret = ext4_find_inline_entry(dir, d_name, res_dir, @@ -1604,6 +1624,6 @@ struct buffer_head *__ext4_find_entry if (has_inline_data) { if (inlined) *inlined = 1; - return ret; + goto out_free; } } @@ -1625,12 +1647,18 @@ struct buffer_head *__ext4_find_entry * return. Otherwise, fall back to doing a search the * old fashioned way. */ - if (err == -ENOENT) - return NULL; + if (err == -ENOENT) { + ret = NULL; + goto out_free; + } - if (err && err != ERR_BAD_DX_DIR) - return ERR_PTR(err); + if (err && err != ERR_BAD_DX_DIR) { + ret = ERR_PTR(err); + goto out_free; + } - if (bh) - return bh; + if (bh) { + ret = bh; + goto out_free; + } dxtrace(printk(KERN_DEBUG "ext4_find_entry: dx failed, " "falling back\n")); ext4_htree_safe_relock(lck); @@ -1667,8 +1698,10 @@ restart: num++; bh = ext4_getblk(NULL, dir, b++, 0, &err); if (unlikely(err)) { - if (ra_max == 0) - return ERR_PTR(err); + if (ra_max == 0) { + ret = ERR_PTR(err); + goto out_free; + } break; } bh_use[ra_max] = bh; @@ -1729,6 +1763,9 @@ cleanup_and_exit: /* Clean up the read-ahead blocks */ for (; ra_ptr < ra_max; ra_ptr++) brelse(bh_use[ra_ptr]); +out_free: + if (qstr.name != name) + kfree(qstr.name); return ret; } EXPORT_SYMBOL(__ext4_find_entry); @@ -2121,7 +2128,7 @@ int ext4_find_dest_de(struct inode *d if (ext4_check_dir_entry(dir, NULL, de, bh, buf, buf_size, offset)) return -EIO; - if (ext4_match(namelen, name, de)) + if (ext4_match(namelen, name, de, de->name_len)) return -EEXIST; nlen = EXT4_DIR_REC_LEN(de); rlen = ext4_rec_len_from_disk(de->rec_len, buf_size);