RT-Thread Version
master
Affected area
RT-Smart
Hardware/BSP vendor
Loongson
Architecture
Not applicable / Other
Board and hardware details
LS2K0300
Develop Toolchain
GCC
Describe the bug
问题现象
LoongArch/LS2K300 启用 RT-Thread Smart 模式后,finsh 启动到 msh 提示符,第一次按键就 page fault:
era = tty_wait_background
fp = NULL
shell 线程首次 read(stdin) 时,进入 tty_wait_background,执行到 pg = p->pgrp 这行,p 是 NULL,直接解引用 → page fault。
调用链
tty_wait_background 是从 ttydisc_read_*(tty_ttydisc.c)一路调上来的,完整调用链:
finsh tshell 线程
→ read(fd) / finsh 调 rt_posix_stdio_get_console() 拿 fd
→ ttydev_read (tty.c)
→ ttydisc_read (tty_ttydisc.c:340)
→ ttydisc_read_canonical / ttydisc_read_raw_*
→ tty_wait_background(tp, curthread, SIGTTIN)
→ p = td->lwp; // ← p == NULL
→ pg = p->pgrp; // ← 解引用 NULL,page fault
触发条件:两个条件同时成立
条件 1:finsh 的 tshell 线程走到 ttydev_read,即读到的是 tty 设备(/dev/ttyS0),而不是 raw 串口(/dev/uart0)。
条件 2:curthread->lwp 为 NULL,即调用者是内核线程而非 lwp 用户态进程。
LoongArch/LS2K300 撞上是因为它同时满足两个条件:Smart 模式下 finsh 切到 /dev/ttyS0(条件 1),finsh 的 tshell 是内核线程(条件 2),首次 read(stdin) 就进 tty_wait_background → pg = p->pgrp 解引用 NULL → page fault。
为什么其他架构没触发
| 架构组合 |
条件 1:finsh 读 tty? |
条件 2:tshell 是内核线程? |
触发? |
| 非 Smart + newlib(raspi/qemu-vexpress) |
✗ finsh 绑 raw /dev/uart0,不走 tty 层 |
✓ 是 |
不触发(条件 1 不成立) |
| Smart + musl(d1/k230/cv18xx) |
✓ finsh 切到 /dev/ttyS0 |
✓ 是 |
理论触发,但没爆出来 |
| Smart + bare-metal(imx6ull/raspi-dm2.0/rockchip) |
看是否切 tty |
✓ 是 |
同上 |
| LoongArch/LS2K300 Smart(已修) |
✓ 切到 /dev/ttyS0 |
✓ 是 |
已触发,已修 |
其他架构没触发,主要是因为:
-
大多数 Smart BSP 还没把 finsh 切到 tty——它们要么没开 Smart,要么 finsh 还绑在 raw /dev/uart0 上,根本没走到 ttydisc_read → tty_wait_background 这条路。
-
只有 finsh 真的去读 /dev/ttyS0、并且 tshell 是内核线程时,才会触发 pg = p->pgrp 的 NULL 解引用。LoongArch/LS2K300 是首批把这条路径走通的 BSP,所以第一个撞到。
根因:BSD tty 代码移植到 RT-Thread 时的遗漏
从代码角度看,tty_wait_background 对 td->lwp == NULL 的处理本来就应该有守卫——这是 BSD tty 代码移植到 RT-Thread 时的一个遗漏。
BSD 原版里 td->td_proc 不会为 NULL(FreeBSD 内核线程也有 proc),但 RT-Thread 的内核线程 lwp 字段是 NULL,所以这个移植性差异在"内核线程读 tty"这个场景下暴露了。
复现环境
- BSP:
bsp/loongarch/ls2k300_dev
- 配置:
CONFIG_RT_USING_SMART=y + musl 工具链 (loongarch64-unknown-linux-musl-)
- 现象: finsh 启动到 msh 提示符后首次按键 page fault,
era = tty_wait_background,fp = NULL
修复方案
补丁在 tty_wait_background() 入口加了一个 NULL 守卫:
// components/lwp/terminal/freebsd/tty.c
int tty_wait_background(struct lwp_tty *tp, struct rt_thread *td, int sig)
{
struct rt_lwp *p;
...
p = td->lwp;
if (p == NULL) // ← 新增守卫
return 0; // ← 内核线程无 lwp,直接放行
for (;;)
{
pg = p->pgrp; // ← 原来这里解引用 NULL 就 page fault
...
}
}
内核线程无 lwp/进程组,本不参与终端前后台作业控制,所以直接返回 0 放行 read/write 是正确的语义。
关键结论
这个 NULL 守卫补丁修的是一个所有 Smart 架构(finsh 走 tty 路径 + tshell 是内核线程)共有的潜在 bug,不是 LoongArch 独有。其他架构没爆出来,主要是因为:
-
大多数 Smart BSP 还没把 finsh 切到 tty——它们要么没开 Smart,要么 finsh 还绑在 raw /dev/uart0 上,根本没走到 ttydisc_read → tty_wait_background 这条路。
-
只有 finsh 真的去读 /dev/ttyS0、并且 tshell 是内核线程时,才会触发 pg = p->pgrp 的 NULL 解引用。LoongArch/LS2K300 是首批把这条路径走通的 BSP,所以第一个撞到。
-
从代码角度看,tty_wait_background 对 td->lwp == NULL 的处理本来就应该有守卫——这是 BSD tty 代码移植到 RT-Thread 时的一个遗漏。BSD 原版里 td->td_proc 不会为 NULL(FreeBSD 内核线程也有 proc),但 RT-Thread 的内核线程 lwp 字段是 NULL,所以这个移植性差异在"内核线程读 tty"这个场景下暴露了。
一句话:这个补丁修的是"内核线程读 tty 时 NULL 解引用"的普遍性 bug,对其他 Smart 架构是预防性修复——只要它们哪天把 finsh 切到 /dev/ttyS0,就会触发同样的 page fault。补丁本身架构无关,加在公共 tty.c 里是正确的位置。
参考
- 修复 commit:
546bbac22c serial/tty: tty_wait_background 加 p==NULL 守卫,内核线程直接放行
- 关键代码位置:
components/lwp/terminal/freebsd/tty.c:414(tty_wait_background 函数)
components/lwp/terminal/freebsd/tty_ttydisc.c:143(调用 tty_wait_background 的位置)
- 详细分析文档:
bsp/loongarch/ls2k300_dev/docs-bak/tty_wait_background_NULL守卫分析.md
Other additional context
No response
RT-Thread Version
master
Affected area
RT-Smart
Hardware/BSP vendor
Loongson
Architecture
Not applicable / Other
Board and hardware details
LS2K0300
Develop Toolchain
GCC
Describe the bug
问题现象
LoongArch/LS2K300 启用 RT-Thread Smart 模式后,finsh 启动到
msh提示符,第一次按键就 page fault:shell 线程首次
read(stdin)时,进入tty_wait_background,执行到pg = p->pgrp这行,p是 NULL,直接解引用 → page fault。调用链
tty_wait_background是从ttydisc_read_*(tty_ttydisc.c)一路调上来的,完整调用链:触发条件:两个条件同时成立
条件 1:finsh 的 tshell 线程走到
ttydev_read,即读到的是 tty 设备(/dev/ttyS0),而不是 raw 串口(/dev/uart0)。条件 2:
curthread->lwp为 NULL,即调用者是内核线程而非 lwp 用户态进程。LoongArch/LS2K300 撞上是因为它同时满足两个条件:Smart 模式下 finsh 切到
/dev/ttyS0(条件 1),finsh 的 tshell 是内核线程(条件 2),首次read(stdin)就进tty_wait_background→pg = p->pgrp解引用 NULL → page fault。为什么其他架构没触发
/dev/uart0,不走 tty 层/dev/ttyS0/dev/ttyS0其他架构没触发,主要是因为:
大多数 Smart BSP 还没把 finsh 切到 tty——它们要么没开 Smart,要么 finsh 还绑在 raw
/dev/uart0上,根本没走到ttydisc_read→tty_wait_background这条路。只有 finsh 真的去读
/dev/ttyS0、并且 tshell 是内核线程时,才会触发pg = p->pgrp的 NULL 解引用。LoongArch/LS2K300 是首批把这条路径走通的 BSP,所以第一个撞到。根因:BSD tty 代码移植到 RT-Thread 时的遗漏
从代码角度看,
tty_wait_background对td->lwp == NULL的处理本来就应该有守卫——这是 BSD tty 代码移植到 RT-Thread 时的一个遗漏。BSD 原版里
td->td_proc不会为 NULL(FreeBSD 内核线程也有 proc),但 RT-Thread 的内核线程lwp字段是 NULL,所以这个移植性差异在"内核线程读 tty"这个场景下暴露了。复现环境
bsp/loongarch/ls2k300_devCONFIG_RT_USING_SMART=y+ musl 工具链 (loongarch64-unknown-linux-musl-)era = tty_wait_background,fp = NULL修复方案
补丁在
tty_wait_background()入口加了一个 NULL 守卫:内核线程无 lwp/进程组,本不参与终端前后台作业控制,所以直接返回 0 放行 read/write 是正确的语义。
关键结论
这个 NULL 守卫补丁修的是一个所有 Smart 架构(finsh 走 tty 路径 + tshell 是内核线程)共有的潜在 bug,不是 LoongArch 独有。其他架构没爆出来,主要是因为:
大多数 Smart BSP 还没把 finsh 切到 tty——它们要么没开 Smart,要么 finsh 还绑在 raw
/dev/uart0上,根本没走到ttydisc_read→tty_wait_background这条路。只有 finsh 真的去读
/dev/ttyS0、并且 tshell 是内核线程时,才会触发pg = p->pgrp的 NULL 解引用。LoongArch/LS2K300 是首批把这条路径走通的 BSP,所以第一个撞到。从代码角度看,
tty_wait_background对td->lwp == NULL的处理本来就应该有守卫——这是 BSD tty 代码移植到 RT-Thread 时的一个遗漏。BSD 原版里td->td_proc不会为 NULL(FreeBSD 内核线程也有 proc),但 RT-Thread 的内核线程lwp字段是 NULL,所以这个移植性差异在"内核线程读 tty"这个场景下暴露了。一句话:这个补丁修的是"内核线程读 tty 时 NULL 解引用"的普遍性 bug,对其他 Smart 架构是预防性修复——只要它们哪天把 finsh 切到
/dev/ttyS0,就会触发同样的 page fault。补丁本身架构无关,加在公共tty.c里是正确的位置。参考
546bbac22cserial/tty: tty_wait_background 加 p==NULL 守卫,内核线程直接放行components/lwp/terminal/freebsd/tty.c:414(tty_wait_background函数)components/lwp/terminal/freebsd/tty_ttydisc.c:143(调用tty_wait_background的位置)bsp/loongarch/ls2k300_dev/docs-bak/tty_wait_background_NULL守卫分析.mdOther additional context
No response