4.19内核在nfs_updatepage
函数中发生空指针解引用。
社区类似问题的邮件: nfs_page_async_flush returning 0 for fatal errors on writeback
相关补丁集: Fix up soft mounts for NFSv4.x
日志:
BUG: unable to handle kernel NULL pointer dereference at 0000000000000080
Call Trace:
nfs_inode_add_request+0x1cc/0x5b8
nfs_setup_write_request+0x1fa/0x1fc
nfs_writepage_setup+0x2d/0x7d
nfs_updatepage+0x8b8/0x936
nfs_write_end+0x61d/0xd45
generic_perform_write+0x19a/0x3f0
nfs_file_write+0x2cc/0x6e5
new_sync_write+0x442/0x560
__vfs_write+0xda/0xef
vfs_write+0x176/0x48b
ksys_write+0x10a/0x1e9
__se_sys_write+0x24/0x29
__x64_sys_write+0x79/0x93
do_syscall_64+0x16d/0x4bb
entry_SYSCALL_64_after_hwframe+0x5c/0xc1
因为合入了14bebe3c90b3 NFS: Don't interrupt file writeout due to fatal errors
补丁,nfs_page_async_flush
函数中在发生致命错误时page->mapping
被设置为空,而nfs_page_async_flush
函数这时不返回错误码,导致nfs_setup_write_request
函数中执行到nfs_inode_add_request
函数,发生了空指针解引用。
write
ksys_write
vfs_write
__vfs_write
new_sync_write
nfs_file_write
generic_perform_write
nfs_write_end
nfs_updatepage
nfs_writepage_setup
nfs_setup_write_request// 尝试搜索已经存在的request,如果已存在就更新,并返回非NULL
// return NULL
nfs_try_to_update_request // return 0
nfs_wb_page if (clear_page_dirty_for_io(page)) // 条件满足
// return 0
nfs_writepage_locked // return 0
nfs_do_writepage // 合入补丁 14bebe3c90b3 NFS: Don't interrupt file writeout due to fatal errors 后返回 0
// return 0
nfs_page_async_flush // 发生致命错误时
nfs_error_is_fatal_on_server
nfs_write_error_remove_page
generic_error_remove_page
truncate_inode_page
truncate_cleanup_page
do_invalidatepage
nfs_invalidate_page
nfs_wb_page_cancel
nfs_inode_remove_request// 清除private标记
ClearPagePrivate(head->wb_page)
delete_from_page_cache
__delete_from_page_cache
page_cache_tree_delete
page->mapping = NULLcontinue
if (clear_page_dirty_for_io(page)) // 条件不满足
if (!PagePrivate(page)) // 条件满足
break
if (req != NULL) // 条件不满足
// 如果不存在就新创建一个request
nfs_create_request// page赋值到新创建的request
req->wb_page = page // 将request与inode关联起来
// 如果 nfs_page_async_flush 不返回0则不执行
nfs_inode_add_request
mapping = page_file_mapping(req->wb_page)return page->mapping
// mapping 为 NULL,发生空指针解引用 spin_lock(&mapping->private_lock)
回退补丁14bebe3c90b3 NFS: Don't interrupt file writeout due to fatal errors
。
回退补丁后,nfs_page_async_flush
函数中在发生致命错误时返回错误码,nfs_setup_write_request
函数中不会执行到nfs_inode_add_request
函数,从而解决空指针解引用问题。
补丁14bebe3c90b3 NFS: Don't interrupt file writeout due to fatal errors
所属的补丁集中还有以下几个相关的补丁:
22876f540bdf NFS: Don't call generic_error_remove_page() while holding locks
6fbda89b257f NFS: Replace custom error reporting mechanism with generic one
补丁集合入后,在最新的代码中, 当nfs_page_async_flush
中产生致命错误时,因为nfs_page_assign_folio
中赋值了新的folio
,nfs_inode_add_request
中的mapping
不会为空,从而不会发生空指针解引用的问题,也不会发生内存泄露。
nfs_setup_write_request
nfs_try_to_update_request
nfs_wb_folio
nfs_writepage_locked
nfs_do_writepage
nfs_page_async_flush// 只记录错误,想留给fsync报给用户态
nfs_write_error
nfs_page_create_from_folio
nfs_page_create
nfs_page_assign_folio// 这个地方保证了不会产生内存泄漏
req->wb_folio = folio
nfs_inode_add_request// 注意这个地方是从folio中取出address_space
struct address_space *mapping = folio_file_mapping(folio)
// 这个地方的mapping一定不会是NULL
spin_lock(&mapping->private_lock)
补丁14bebe3c90b3 NFS: Don't interrupt file writeout due to fatal errors
里描述的: 不立刻上报回写错误,而是让fsync
上报。
要说明一下,maintainer自己都没想好这个机制有没问题,具体可以参考我的另一篇文章: 《nfs回写错误处理不正确的问题》,这里就不做过多展开。
而且这是一个重构补丁集,根本就没必要在这里加一个“Fixes:”标签,补丁集是为了改变一个机制,并不是为了解决一个bug。这个补丁之所以会被Greg Kroah-Hartman错误的单独合入到4.19中,就是因为nfs maintainer喜欢乱加“Fixes:”标签,这已经不是他第一次这样做了。
4.19代码合入补丁0001-reproduce-4.19-null-ptr-deref-in-nfs_updatepage.patch。
挂载nfs:
mount -t nfs -o vers=4.1 ${nfs_server_ip}:/server/export/dir /mnt
不断执行以下脚本,直到发生空指针解引用:
echo something > something
echo something_else > something_else
echo something_else_again > something_else_again
# 为什么不直接用 echo something > /mnt/file 呢,因为用ps无法查看到echo进程
cat something > /mnt/file &
cat something_else > /mnt/file &
cat something_else_again > /mnt/file &
Question about LTS 4.19 patch “89047634f5ce NFS: Don’t interrupt file writeout due to fatal errors”
Trond回复:
根据定义,重构是一种不影响代码行为的更改。很明显,这个补丁从未被设计为这样的一个补丁。
出现问题的原因是在4.19.x版本中发生了错误,而在最新版本的内核中没有发生,这是因为前者缺少另一个修复错误的补丁(实际上缺少一个'Fixes:'标签)。
因此,您是否可以检查一下是否应用提交 22876f540bdf ("NFS: Don't call generic_error_remove_page() while holding locks") 可以修复这个问题。
请注意,为了解决读取死锁问题(如标签上所指示的),无论如何都需要后一个补丁。
打上这个补丁后是有问题的,会进入死循环,具体请查看《4.19 nfs_wb_page() soft lockup的问题》。
# 查看内核日志
crash> dmesg | less
...
BUG: unable to handle kernel NULL pointer dereference at 0000000000000080
...
RIP: 0010:_raw_spin_lock+0x1d/0x35
...
nfs_inode_add_request+0x1cc/0x5b8
# 在内核仓库目录下执行的shell命令,在docker ubuntu2204环境中打印不出具体行号,原因暂时母鸡
./scripts/faddr2line build/vmlinux nfs_inode_add_request+0x1cc/0x5b8 # 或者把vmlinux替换成ko文件
nfs_inode_add_request+0x1cc/0x5b8:
nfs_have_writebacks at include/linux/nfs_fs.h:548 (discriminator 3)
(inlined by) nfs_inode_add_request at fs/nfs/write.c:774 (discriminator 3)
# 反汇编指定地址的代码,以查看其汇编指令, -l 选项用于在反汇编时显示源代码行号(如果可用)
crash> dis -l ffffffff81c0a939
/home/sonvhi/chenxiaosong/code/4.19-stable/build/../include/linux/nfs_fs.h: 248
0xffffffff81c0a939 <nfs_inode_add_request+460>: lea -0xe0(%rbp),%r14
crash> bt # 查看崩溃的栈
exception RIP: _raw_spin_lock+29] RIP: ffffffff836c0e4a
[...
ffff8880b1ab7a78] nfs_inode_add_request at ffffffff81c0a939
[ffff8880b1ab7ab0] nfs_setup_write_request at ffffffff81c14312
[
# 查找指定地址的符号信息
crash> sym ffffffff836c0e4a # [exception RIP: _raw_spin_lock+29] RIP: ffffffff836c0e4a
ffffffff836c0e4a (T) _raw_spin_lock+29 /home/sonvhi/chenxiaosong/code/4.19-stable/build/../arch/x86/include/asm/atomic.h: 194
crash> sym ffffffff81c0a939 # [ffff8880b1ab7a78] nfs_inode_add_request at ffffffff81c0a939
ffffffff81c0a939 (t) nfs_inode_add_request+460 /home/sonvhi/chenxiaosong/code/4.19-stable/build/../include/linux/nfs_fs.h: 248
faddr2line
脚本解析是的nfs_inode_add_request
函数中的774行,也就是发生问题的spin_lock(&mapping->private_lock)
之后的一行,看来解析还是有一点点的不准,不过不要紧,我们再看dmesg
命令中的日志RIP: 0010:_raw_spin_lock+0x1d/0x35
,就能确定确实是崩溃在773行spin_lock(&mapping->private_lock)
。
另外,由crash的dis -l
和sym
命令定位到include/linux/nfs_fs.h: 248
的NFS_I()
函数,这就不知道为什么了,麻烦知道的朋友联系我。
page->mapping
为NULL
查看nfs_inode_add_request
函数中的堆栈:
# -F[F]: 类似于 -f,不同之处在于当适用时以符号方式显示堆栈数据;如果堆栈数据引用了 slab cache 对象,将在方括号内显示 slab cache 的名称;在 ia64 架构上,将以符号方式替代参数寄存器的内容。如果输入 -F 两次,并且堆栈数据引用了 slab cache 对象,将同时显示地址和 slab cache 的名称在方括号中。
crash> bt -FF
#10 [ffff8880b1ab79c0] async_page_fault at ffffffff8380119e
exception RIP: _raw_spin_lock+29]
[...
RDX: 0000000000000001 RSI: ffff8880b5760000 RDI: 0000000000000002
...
#11 [ffff8880b1ab7a78] nfs_inode_add_request at ffffffff81c0a939
ffff8880b1ab7a80: ffffea0002cd5900 [ffff8880b0cf0b00:nfs_page]
...
crash> bt -F
#11 [ffff8880b1ab7a78] nfs_inode_add_request at ffffffff81c0a939
ffff8880b1ab7a80: ffffea0002cd5900 [nfs_page]
# -f: 显示堆栈帧中包含的所有数据;此选项可用于确定传递给每个函数的参数;在 ia64 架构上,将显示参数寄存器的内容。
crash> bt -f
#11 [ffff8880b1ab7a78] nfs_inode_add_request at ffffffff81c0a939
ffff8880b1ab7a80: ffffea0002cd5900 ffff8880b0cf0b00
注意[ffff8880b0cf0b00:nfs_page]
并不一定是nfs_page
结构体的起始地址,有可能是中间某个变量的地址,要用kmem
命令查看:
crash> kmem ffff8880b0cf0b00
...
FREE / [ALLOCATED] # 中括号[ALLOCATED]代表已分配
ffff8880b0cf0b00]
[...
只是这里刚好是起始地址,使用地址ffff8880b0cf0b00
来查看结构体nfs_page
中的数据:
crash> struct nfs_page ffff8880b0cf0b00 -x
struct nfs_page {
...
wb_page = 0xffffea0002cd5900,
...
}
再查看wb_page = 0xffffea0002cd5900
中的数据:
struct page {
...
{
{
...
mapping = 0x0,
...
},
}
}
再看发生崩溃的地方:
nfs_inode_add_request// static __always_inline void spin_lock(spinlock_t *lock)
spin_lock(lock = &mapping->private_lock)
raw_spin_lock(&lock->rlock)// void __lockfunc _raw_spin_lock(raw_spinlock_t *lock) _raw_spin_lock(&lock->rlock)
再解析结构体偏移:
# 首先已知 struct address_space *mapping = 0
crash> struct address_space -ox
struct address_space {
...
# 这里正好和dmesg日志中对应上: BUG: unable to handle kernel NULL pointer dereference at 0000000000000080
# 当然,如果mapping的地址是一个不为NULL的值,比如是一个无效的地址x,那报错的地址就是0000000000000080+x
0x80] spinlock_t private_lock;
[...
}SIZE: 0xa8
crash> struct spinlock_t -ox
typedef struct spinlock {
union {
0x0] struct raw_spinlock rlock;
[
};spinlock_t;
} SIZE: 0x4
x86_64下整数参数使用的寄存器依次为: RDI,RSI,RDX,RCX,R8,R9,_raw_spin_lock
只有一个参数,从栈中可以看到RDI: 0000000000000002
,这个值是怎么来的呢?估计要看nfs_inode_add_request
和_raw_spin_lock
的反汇编。
nfs_inode_add_request
的第一个参数struct inode *inode
我们再看一次跟inode
有关的栈帧数据:
crash> bt -FF
#10 [ffff8880b1ab79c0] async_page_fault at ffffffff8380119e
exception RIP: _raw_spin_lock+29]
[...
ffff8880b1ab79c8: [ffff8880b6a43ec8:nfs_inode_cache(198:serial-getty@ttyS0.service)] ffffea0002cd5900
...
ffff8880b1ab79e8: [ffff8880b6a43ec8:nfs_inode_cache(198:serial-getty@ttyS0.service)] 0000000000000080
...
#11 [ffff8880b1ab7a78] nfs_inode_add_request at ffffffff81c0a939
...
ffff8880b1ab7aa0: [ffff888107060a80:kmalloc-128] [ffff8880b6a43ec8:nfs_inode_cache(198:serial-getty@ttyS0.service)]
...
注意地址[ffff8880b6a43ec8:nfs_inode_cache]
并不是struct nfs_inode
的首地址,而是struct inode
的首地址,使用kmem
命令解析:
crash> kmem ffff8880b6a43ec8
...
FREE / [ALLOCATED]
ffff8880b6a43cf0] # 这才是struct nfs_inode的首地址
[...
crash> struct nfs_inode ffff8880b6a43cf0 -ox
struct nfs_inode {
...
ffff8880b6a43ec8] struct inode vfs_inode; # 这就是struct page的地址
[
}SIZE: 0x430
crash> struct inode ffff8880b6a43ec8 -x
struct inode {
...
i_mapping = 0xffff8880b6a44040,
...
inode->i_mapping
的值和nfs_setup_write_request
栈中的一个数据一样,以后再研究他们之间的关系吧:
#12 [ffff8880b1ab7ab0] nfs_setup_write_request at ffffffff81c14312
...
ffff8880b1ab7ad8: [ffff8880b6a44040:nfs_inode_cache(198:serial-getty@ttyS0.service)] 000000000000000f