Bug 26936 - asyncfork中关于write_protect_seq信号量bugon问题上报和讨论
Summary: asyncfork中关于write_protect_seq信号量bugon问题上报和讨论
Status: NEW
Alias: None
Product: ANCK 6.6 Dev
Classification: ANCK
Component: mm (show other bugs) mm
Version: unspecified
Hardware: All Linux
: P3-Medium S3-normal
Target Milestone: ---
Assignee: baolinwang
QA Contact: shuming
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2025-11-06 16:14 UTC by zhangqilong
Modified: 2025-11-06 16:48 UTC (History)
3 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description zhangqilong 2025-11-06 16:14:36 UTC
Description of problem:
请教一个asyncfork的问题。打开以下开关

CONFIG_ASYNC_FORK=y
CONFIG_DEBUG_VM=y

然后做asyncfork的并发测试,出现以下bug_on
Modules linked in:
CPU: 6 PID: 510 Comm: a.out Not tainted 6.6.0+ #238
kernel BUG at include/linux/mm.h:2070!
Hardware name: QEMU QEMU Virtual Machine, BIOS 0.0.0 02/06/2015
...
Call trace:
 copy_pte_range+0x2518/0x6be8
 __copy_page_range+0x428/0xdd0
 async_fork_fixup_pmd+0x3c4/0xa90
 __handle_mm_fault+0x28c/0x950
 handle_mm_fault+0x110/0x958
 do_page_fault+0x698/0xd70

bugon的代码是:
folio_needs_cow_for_dma
{
    VM_BUG_ON(!(raw_read_seqcount(&vma->vm_mm->write_protect_seq) & 1));
    ...
}

看了一下BUGON的原因,是因为copy_pte_range流程中,没有增加vma->vm_mm->write_protect_seq的信号量保护。

看了一下社区引入write_protect_seq的历史,是为了保护fork流程与pinned流程的并发,防止pinned返回不正确的结果,该信号量是一个只增不减的信号量,初始值为0,fork流程中页表拷贝之前信号量加一,拷贝结束,信号量再加一。如果其二进制值第0位为1,表示信号量为奇数,代表处于fork的页表拷贝流程中。

asyncfork引入之后,导致页表拷贝的触发点变多了,导致这种对应关系(如果为奇数,表示处于页表拷贝流程中,如果是偶数,代表没有处于页表拷贝,pinned功能不受影响)被破坏。

asyncfork实现考虑到了这个问题(点赞!)。代码流程中使用seq_should_lock = false来解决问题,场景是如果当前处于asyncfork的CPR_SLOW模式页表拷贝上下文,并且async_fork_refcnt计数不为1,表示前面已经有进程也处于CPR_SLOW模式页表拷贝上下文,这种情况下认为不再需要进行write_protect_seq的保护了,即不增加write_protect_seq的信号量。

但是上面的修复还是有问题的。进程A首先进入了asyncfork的CPR_SLOW模式页表拷贝上下文,增加了write_protect_seq的信号量,拷贝还未结束时,进程B也进来了(能并发的原因很多地方都是持有mm的读锁)到了这里,发现A已经加过了,进程B就不加了,但是此时如果很快A结束了,B进程还在拷贝,那么此时进程B的页表拷贝就会出现社区修复的问题,与pinned并发会有问题。时序图如下

-------------------------------------------------------------------------------
进程A                   进程B                         进程C 
mode = CPR_SLOW         mode = CPR_SLOW

add write_protect_seq

                        not add write_protect_seq

copy page table

sub write_protect_seq

                                                     get_user_pages_fast()
                        
                        copy page table
                        atomic_read(has_pinned) == 0
                        page_maybe_dma_pinned() == false

                                                    atomic_set(has_pinned, 1);

                        pte = pte_wrprotect(pte)
-------------------------------------------------------------------------------
综上所述,主要是两个问题:
1)如何避免开启CONFIG_DEBUG_VM开关之后的bugon问题;
2)目前分析asyncfork没有完全解决write_protect_seq信号量的问题,与user pinned的流程并发还是存在社区描述的问题。
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=57efa1fe5957694fa541c9062de0a127f0b9acb0

这个是基于龙蜥开源代码的分析,如果你们已有修复或者不妥当的地方敬请大佬指出,感谢!

How reproducible:

开启asyncfork功能,fork之后,在父进程中进行并发的父进程数据修改,触发COW机制,回退asyncfork的页表


Steps to Reproduce:
1.开启asyncfork控制开关
2.mmap一段4M内存不使用大页,然后写入数据,然后fork()
3.父进程中至少2个线程,并发写访问步骤2的地址,高概率复现该问题

Actual results:
内核panic

Expected results:
系统正常运行

Additional info:
Comment 1 zhangqilong 2025-11-06 16:22:47 UTC
(In reply to zhangqilong from comment #0)
> Description of problem:
> 请教一个asyncfork的问题。打开以下开关
> 
> CONFIG_ASYNC_FORK=y
> CONFIG_DEBUG_VM=y
> 
> 然后做asyncfork的并发测试,出现以下bug_on
> Modules linked in:
> CPU: 6 PID: 510 Comm: a.out Not tainted 6.6.0+ #238
> kernel BUG at include/linux/mm.h:2070!
> Hardware name: QEMU QEMU Virtual Machine, BIOS 0.0.0 02/06/2015
> ...
> Call trace:
>  copy_pte_range+0x2518/0x6be8
>  __copy_page_range+0x428/0xdd0
>  async_fork_fixup_pmd+0x3c4/0xa90
>  __handle_mm_fault+0x28c/0x950
>  handle_mm_fault+0x110/0x958
>  do_page_fault+0x698/0xd70
> 
> bugon的代码是:
> folio_needs_cow_for_dma
> {
>     VM_BUG_ON(!(raw_read_seqcount(&vma->vm_mm->write_protect_seq) & 1));
>     ...
> }
> 
> 看了一下BUGON的原因,是因为copy_pte_range流程中,没有增加vma->vm_mm->write_protect_seq的信号量保护。
> 
> 看了一下社区引入write_protect_seq的历史,是为了保护fork流程与pinned流程的并发,防止pinned返回不正确的结果,
> 该信号量是一个只增不减的信号量,初始值为0,fork流程中页表拷贝之前信号量加一,拷贝结束,信号量再加一。
> 如果其二进制值第0位为1,表示信号量为奇数,代表处于fork的页表拷贝流程中。
> 
> asyncfork引入之后,导致页表拷贝的触发点变多了,导致这种对应关系(如果为奇数,表示处于页表拷贝流程中,如果是偶数,代表没有处于页表拷贝,pinne
> d功能不受影响)被破坏。
> 
> asyncfork实现考虑到了这个问题(点赞!)。代码流程中使用seq_should_lock =
> false来解决问题,场景是如果当前处于asyncfork的CPR_SLOW模式页表拷贝上下文,并且async_fork_refcnt计数不为1,表示前面
> 已经有进程也处于CPR_SLOW模式页表拷贝上下文,这种情况下认为不再需要进行write_protect_seq的保护了,即不增加write_protec
> t_seq的信号量。
> 
> 但是上面的修复还是有问题的。
> 进程A首先进入了asyncfork的CPR_SLOW模式页表拷贝上下文,增加了write_protect_seq的信号量,拷贝还未结束时,进程B也进来了(
> 能并发的原因很多地方都是持有mm的读锁)到了这里,发现A已经加过了,进程B就不加了,但是此时如果很快A结束了,B进程还在拷贝,那么此时进程B的页表拷贝就会
> 出现社区修复的问题,与pinned并发会有问题。时序图如下
> 
> -----------------------------------------------------------------------------
> --
> 进程A                   进程B                         进程C 

这里写错了,应该是线程

> mode = CPR_SLOW         mode = CPR_SLOW
> 
> add write_protect_seq
> 
>                         not add write_protect_seq
> 
> copy page table
> 
> sub write_protect_seq
> 
>                                                      get_user_pages_fast()
>                         
>                         copy page table
>                         atomic_read(has_pinned) == 0
>                         page_maybe_dma_pinned() == false
> 
>                                                     atomic_set(has_pinned,
> 1);
> 
>                         pte = pte_wrprotect(pte)
> -----------------------------------------------------------------------------
> --
> 综上所述,主要是两个问题:
> 1)如何避免开启CONFIG_DEBUG_VM开关之后的bugon问题;
> 2)目前分析asyncfork没有完全解决write_protect_seq信号量的问题,与user pinned的流程并发还是存在社区描述的问题。
> https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/
> ?id=57efa1fe5957694fa541c9062de0a127f0b9acb0
> 
> 这个是基于龙蜥开源代码的分析,如果你们已有修复或者不妥当的地方敬请大佬指出,感谢!
> 
> How reproducible:
> 
> 开启asyncfork功能,fork之后,在父进程中进行并发的父进程数据修改,触发COW机制,回退asyncfork的页表
> 
> 
> Steps to Reproduce:
> 1.开启asyncfork控制开关
> 2.mmap一段4M内存不使用大页,然后写入数据,然后fork()
> 3.父进程中至少2个线程,并发写访问步骤2的地址,高概率复现该问题
> 
> Actual results:
> 内核panic
> 
> Expected results:
> 系统正常运行
> 
> Additional info:
Comment 2 zhangqilong 2025-11-06 16:48:42 UTC
(In reply to zhangqilong from comment #0)
> Description of problem:
> 请教一个asyncfork的问题。打开以下开关
> 
> CONFIG_ASYNC_FORK=y
> CONFIG_DEBUG_VM=y
> 
> 然后做asyncfork的并发测试,出现以下bug_on
> Modules linked in:
> CPU: 6 PID: 510 Comm: a.out Not tainted 6.6.0+ #238
> kernel BUG at include/linux/mm.h:2070!
> Hardware name: QEMU QEMU Virtual Machine, BIOS 0.0.0 02/06/2015
> ...
> Call trace:
>  copy_pte_range+0x2518/0x6be8
>  __copy_page_range+0x428/0xdd0
>  async_fork_fixup_pmd+0x3c4/0xa90
>  __handle_mm_fault+0x28c/0x950
>  handle_mm_fault+0x110/0x958
>  do_page_fault+0x698/0xd70
> 
> bugon的代码是:
> folio_needs_cow_for_dma
> {
>     VM_BUG_ON(!(raw_read_seqcount(&vma->vm_mm->write_protect_seq) & 1));
>     ...
> }
> 
> 看了一下BUGON的原因,是因为copy_pte_range流程中,没有增加vma->vm_mm->write_protect_seq的信号量保护。
> 
> 看了一下社区引入write_protect_seq的历史,是为了保护fork流程与pinned流程的并发,防止pinned返回不正确的结果,
> 该信号量是一个只增不减的信号量,初始值为0,fork流程中页表拷贝之前信号量加一,拷贝结束,信号量再加一。
> 如果其二进制值第0位为1,表示信号量为奇数,代表处于fork的页表拷贝流程中。
> 
> asyncfork引入之后,导致页表拷贝的触发点变多了,导致这种对应关系(如果为奇数,表示处于页表拷贝流程中,如果是偶数,代表没有处于页表拷贝,pinne
> d功能不受影响)被破坏。
> 
> asyncfork实现考虑到了这个问题(点赞!)。代码流程中使用seq_should_lock =
> false来解决问题,场景是如果当前处于asyncfork的CPR_SLOW模式页表拷贝上下文,并且async_fork_refcnt计数不为1,表示前面
> 已经有进程也处于CPR_SLOW模式页表拷贝上下文,这种情况下认为不再需要进行write_protect_seq的保护了,即不增加write_protec
> t_seq的信号量。
> 
> 但是上面的修复还是有问题的。
> 进程A首先进入了asyncfork的CPR_SLOW模式页表拷贝上下文,增加了write_protect_seq的信号量,拷贝还未结束时,进程B也进来了(
> 能并发的原因很多地方都是持有mm的读锁)到了这里,发现A已经加过了,进程B就不加了,但是此时如果很快A结束了,B进程还在拷贝,那么此时进程B的页表拷贝就会
> 出现社区修复的问题,与pinned并发会有问题。时序图如下
> 
> -----------------------------------------------------------------------------
> --
> 进程A                   进程B                         进程C 
> mode = CPR_SLOW         mode = CPR_SLOW
> 
> add write_protect_seq
> 
>                         not add write_protect_seq
> 
> copy page table
> 
> sub write_protect_seq
> 
>                                                      get_user_pages_fast()
>                         
>                         copy page table
>                         atomic_read(has_pinned) == 0
>                         page_maybe_dma_pinned() == false
> 
>                                                     atomic_set(has_pinned,
> 1);
> 
>                         pte = pte_wrprotect(pte)
> -----------------------------------------------------------------------------
> --
> 综上所述,主要是两个问题:
> 1)如何避免开启CONFIG_DEBUG_VM开关之后的bugon问题;
> 2)目前分析asyncfork没有完全解决write_protect_seq信号量的问题,与user pinned的流程并发还是存在社区描述的问题。

看到了以下适配:
->gup_pte_range // 已经获取页表锁
 ->if (is_pmd_copied_slow(pmd))

貌似可以解决这个问题,我再想想。


> https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/
> ?id=57efa1fe5957694fa541c9062de0a127f0b9acb0
> 
> 这个是基于龙蜥开源代码的分析,如果你们已有修复或者不妥当的地方敬请大佬指出,感谢!
> 
> How reproducible:
> 
> 开启asyncfork功能,fork之后,在父进程中进行并发的父进程数据修改,触发COW机制,回退asyncfork的页表
> 
> 
> Steps to Reproduce:
> 1.开启asyncfork控制开关
> 2.mmap一段4M内存不使用大页,然后写入数据,然后fork()
> 3.父进程中至少2个线程,并发写访问步骤2的地址,高概率复现该问题
> 
> Actual results:
> 内核panic
> 
> Expected results:
> 系统正常运行
> 
> Additional info: