Analysis:
---

Kernel BUG:

        Apr 21 11:39:31 web-12667 kernel: BUG: unable to handle kernel NULL 
pointer dereference at 00000000000002c0
        Apr 21 11:39:31 web-12667 kernel: IP: [<ffffffff8132ae16>] 
fuse_readdir+0x376/0x700
        ...
        Apr 21 11:39:31 web-12667 kernel: Oops: 0000 [#5] SMP
        ...
        Apr 21 11:39:31 web-12667 kernel: CPU: 1 PID: 12133 Comm: php-fpm 
Tainted: G D 4.4.0-1138-aws #152-Ubuntu

(an internal kern.log shows 7 of these, with consistent function+offset
and faulting address.)


Faulting source code line:

        $ eu-addr2line -ifae vmlinux-4.4.0-1138-aws fuse_readdir+0x376
        0xffffffff8132ae16

        constant_test_bit inlined at 
/build/linux-aws-8TZ6DM/linux-aws-4.4.0/fs/fuse/fuse_i.h:699 in fuse_readdir
        
/build/linux-aws-8TZ6DM/linux-aws-4.4.0/arch/x86/include/asm/bitops.h:311

        fuse_is_bad
        /build/linux-aws-8TZ6DM/linux-aws-4.4.0/fs/fuse/fuse_i.h:699

        fuse_direntplus_link
        /build/linux-aws-8TZ6DM/linux-aws-4.4.0/fs/fuse/dir.c:1267

        parse_dirplusfile
        /build/linux-aws-8TZ6DM/linux-aws-4.4.0/fs/fuse/dir.c:1342

        fuse_readdir
        /build/linux-aws-8TZ6DM/linux-aws-4.4.0/fs/fuse/dir.c:1392

Source code:

Issue: 'if (!inode)' doesn't prevent 'if (fuse_is_bad(inode)' from
running with 'inode == NULL'.

        1194 static int fuse_direntplus_link(struct file *file,
        1195                                 struct fuse_direntplus *direntplus,
        1196                                 u64 attr_version)
        1197 {
        ...
        1240         if (dentry) {
        1241                 inode = d_inode(dentry);
        1242                 if (!inode) {
        1243                         d_drop(dentry);
        1244                 } else if ...
        1246                         ...
        1247                 } else if (is_bad_inode(inode)) {
        1248                         err = -EIO;
        1249                         goto out;
        1250                 } else {
        1251                         ...
        1266                 }
        1267                 if (fuse_is_bad(inode)) {
        ...
        1272         }

        697 static inline bool fuse_is_bad(struct inode *inode)
        698 {
        699         return unlikely(test_bit(FUSE_I_BAD, 
&get_fuse_inode(inode)->state));
        700 }

        681 static inline struct fuse_inode *get_fuse_inode(struct inode *inode)
        682 {
        683         return container_of(inode, struct fuse_inode, inode);
        684 }

Struct field offset (matches faulting address, i.e., NULL + 0x2c0)

        $ pahole --hex -C fuse_inode vmlinux-4.4.0-1138-aws  | grep state
                long unsigned int          state;                /* 0x2c0   0x8 
*/

The origin of the issue is in the backport to linux-xenial from linux-stable 
4.9,
which comes from linux mainline.

- mainline: 
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=5d069dbe8aaf2a197142558b6fb2978189ba3454
- stable49: 
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/fs/fuse/dir.c?h=v4.9.311&id=3a2f8823aa565cc67bdd00c4cd5e1d8ad81e8436
- backport: 
https://git.launchpad.net/~canonical-kernel-esm/canonical-kernel-esm/+git/linux-xenial/commit/?h=master-prep&id=8deb786162e1e4cf73ae4c56d62b86f0d7c8ade0

In mainline there's no fuse_direntplus_link()

In stable/4.9, there's an '!inode' check w/ 'goto retry' that skips 
'fuse_is_bad()'
- 
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/fuse/dir.c?h=v4.9.311&id=3a2f8823aa565cc67bdd00c4cd5e1d8ad81e8436#n1268

        ~/git/linux-stable$ git log --oneline origin/linux-4.9.y  -- 
fs/fuse/dir.c | head -n1
        3a2f8823aa56 fuse: fix bad inode
                
        ~/git/linux-stable$ git show -U10 3a2f8823aa56 | grep -A15 
fuse_direntplus_link
        @@ -1244,21 +1265,21 @@ static int fuse_direntplus_link(struct file 
*file,
                if (!d_in_lookup(dentry)) {
                        struct fuse_inode *fi;
                        inode = d_inode(dentry);
                        if (!inode ||
                            get_node_id(inode) != o->nodeid ||
                            ((o->attr.mode ^ inode->i_mode) & S_IFMT)) {
                                d_invalidate(dentry);
                                dput(dentry);
                                goto retry;
                        }
        -               if (is_bad_inode(inode)) {
        +               if (fuse_is_bad(inode)) {
                                dput(dentry);
                                return -EIO;
                        }

In xenial, there's no such 'goto' or another mechanism to skip
'fuse_is_bad(inode)' if '!inode' (above).

-- 
You received this bug notification because you are a member of Ubuntu
Bugs, which is subscribed to Ubuntu.
https://bugs.launchpad.net/bugs/1970482

Title:
  Xenial: kernel BUG/Oops/crash in fuse_readdir() due to CVE-2020-36322
  backport

To manage notifications about this bug go to:
https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1970482/+subscriptions


-- 
ubuntu-bugs mailing list
[email protected]
https://lists.ubuntu.com/mailman/listinfo/ubuntu-bugs

Reply via email to