Linux内核垃圾回收的量子态:CVE-2021-0920(第一部分)
深入分析一个野外Android漏洞利用
作者:Xingyu Jin,Android安全研究
这是由两部分组成的客座博客文章的第一部分,我们将首先探讨CVE-2021-0920漏洞的根本原因。在第二篇文章中,我们将深入分析该漏洞的野外0day利用以及入侵后模块。
野外CVE-2021-0920漏洞利用概述
一家名为Wintego的监控供应商开发了针对Linux socket系统调用0day漏洞CVE-2021-0920的利用程序,并根据最早捕获的样本,至少在2020年11月就开始在野外使用,直到2021年11月该问题被修复。结合Chrome和三星浏览器漏洞利用,该供应商能够远程获取三星设备的root权限。修复程序随2021年11月Android安全公告发布,并在三星2021年12月的安全更新中应用于三星设备。
Google威胁分析小组(TAG)发现了在野外使用的三星浏览器漏洞利用链。TAG随后进行了根本原因分析,发现该漏洞CVE-2021-0920被用于逃逸沙箱并提升权限。CVE-2021-0920被匿名报告给Linux/Android。Google Android安全团队对该漏洞利用进行了全面的深入分析。
这个问题最初由RedHat内核开发人员在2016年发现,并在公开电子邮件线程中披露,但Linux内核社区没有修补该问题,直到2021年重新报告。
各种三星设备成为攻击目标,包括三星S10和S20。通过滥用Linux内核垃圾回收中的一个短暂竞争条件,漏洞利用代码能够在内核sk_buff对象中获得一个释放后使用(UAF)。野外样本能够有效绕过CONFIG_ARM64_UAO,实现任意读/写原语,并绕过三星RKP以提升到root权限。其他Android设备也容易受到攻击,但我们没有发现针对它们的任何漏洞利用样本。
从捕获的样本中提取的文本将该漏洞称为“量子Linux内核垃圾回收”,这似乎是这篇博客文章的合适标题。
引言
CVE-2021-0920是一个由于SCM_RIGHTS垃圾回收系统中的竞争条件导致的释放后使用(UAF)。SCM_RIGHTS是一种控制消息,允许Unix域套接字将打开的文件描述符从一个进程传输到另一个进程。换句话说,发送方传输一个文件描述符,接收方然后从发送方获取一个文件描述符。这种文件描述符的传递增加了文件结构引用计数的复杂性。为了解决这个问题,Linux内核社区设计了一个特殊的垃圾回收系统。CVE-2021-0920就是这个垃圾回收系统中的一个漏洞。通过在垃圾回收过程中赢得竞争条件,攻击者可以利用套接字缓冲区sk_buff对象上的UAF。在接下来的部分中,我们将解释SCM_RIGHTS垃圾回收系统以及漏洞的细节。分析基于Linux 4.14内核。
什么是SCM_RIGHTS?
Linux开发人员可以使用SCM_RIGHTS数据报和sendmsg系统调用将文件描述符(fd)从一个进程共享到另一个进程。当一个进程将文件描述符传递给另一个进程时,SCM_RIGHTS将增加对底层文件结构的引用。这意味着发送文件描述符的进程可以立即关闭其端的文件描述符,即使接收进程尚未接受并取得文件描述符的所有权。当文件描述符处于“排队”状态时(意味着发送方已传递fd然后关闭它,但接收方尚未接受fd并取得所有权),需要专门的垃圾回收。为了跟踪这种“排队”状态,这篇LWN文章很好地解释了SCM_RIGHTS引用计数,建议在继续阅读本博客文章之前先阅读它。
发送
如前所述,Unix域套接字使用系统调用sendmsg将文件描述符发送到另一个套接字。为了解释SCM_RIGHTS期间的引用计数,我们将从发送方的角度开始。我们从内核函数unix_stream_sendmsg开始,它实现了sendmsg系统调用。为了实现SCM_RIGHTS功能,内核使用结构scm_fp_list来管理所有传输的文件结构。scm_fp_list存储要传递的文件指针列表。
struct scm_fp_list {
short count;
short max;
struct user_struct *user;
struct file *fp[SCM_MAX_FD];
};
unix_stream_sendmsg调用scm_send(af_unix.c#L1886)来初始化scm_fp_list结构,该结构通过堆栈上的scm_cookie结构链接。
struct scm_cookie {
struct pid pid; / Skb credentials */
struct scm_fp_list fp; / Passed files /
struct scm_creds creds; / Skb credentials /
#ifdef CONFIG_SECURITY_NETWORK
u32 secid; / Passed security ID */
#endif
};
更具体地说,scm_send → __scm_send → scm_fp_copy(scm.c#L68)从用户空间读取文件描述符并初始化scm_cookie->fp,它可以包含SCM_MAX_FD个文件结构。
由于Linux内核使用sk_buff(也称为套接字缓冲区或skb)对象来管理所有类型的套接字数据报,内核还需要调用unix_scm_to_skb函数将scm_cookie->fp链接到相应的skb对象。这发生在unix_attach_fds(scm.c#L103)中:
…
/*
- Need to duplicate file references for the sake of garbage
- collection. Otherwise a socket in the fps might become a
- candidate for GC while the skb is not yet queued.
*/
UNIXCB(skb).fp = scm_fp_dup(scm->fp);
if (!UNIXCB(skb).fp)
return -ENOMEM;
…
unix_attach_fds中的scm_fp_dup调用增加正在传递的文件描述符的引用计数,因此即使发送方稍后关闭传输的文件描述符,文件仍然有效:
struct scm_fp_list *scm_fp_dup(struct scm_fp_list *fpl)
{
struct scm_fp_list *new_fpl;
int i;
if (!fpl)
return NULL;
new_fpl = kmemdup(fpl, offsetof(struct scm_fp_list, fp[fpl->count]),
GFP_KERNEL);
if (new_fpl) {
for (i = 0; i < fpl->count; i++)
get_file(fpl->fp[i]);
new_fpl->max = new_fpl->count;
new_fpl->user = get_uid(fpl->user);
}
return new_fpl;
}
让我们检查一个具体示例。假设我们有套接字A和B。A尝试将自己传递给B。发送SCM_RIGHTS数据报后,来自发送方的新分配skb将被附加到B的sk_receive_queue,该队列存储接收到的数据报:

sk_buff携带scm_fp_list结构
A的引用计数增加到2,B的引用计数仍为1。
接收
现在,让我们看看接收方unix_stream_read_generic(我们暂时不讨论MSG_PEEK标志,专注于正常例程)。首先,内核使用skb_peek从sk_receive_queue获取当前skb。其次,由于scm_fp_list附加到skb,内核将调用unix_detach_fds(链接)从skb解析传输的文件结构,并从sk_receive_queue清除skb:
/* Mark read part of skb as used */
if (!(flags & MSG_PEEK)) {
UNIXCB(skb).consumed += chunk;
sk_peek_offset_bwd(sk, chunk);
if (UNIXCB(skb).fp)
unix_detach_fds(&scm, skb);
if (unix_skb_len(skb))
break;
skb_unlink(skb, &sk->sk_receive_queue);
consume_skb(skb);
if (scm.fp)
break;
}
函数scm_detach_fds遍历传递的文件描述符列表(scm->fp)并为接收方相应地安装新的文件描述符:
for (i=0, cmfptr=(__force int __user )CMSG_DATA(cm); i<fdmax;
i++, cmfptr++)
{
struct socket *sock;
int new_fd;
err = security_file_receive(fp[i]);
if (err)
break;
err = get_unused_fd_flags(MSG_CMSG_CLOEXEC & msg->msg_flags
? O_CLOEXEC : 0);
if (err < 0)
break;
new_fd = err;
err = put_user(new_fd, cmfptr);
if (err) {
put_unused_fd(new_fd);
break;
}
/ Bump the usage count and install the file. /
sock = sock_from_file(fp[i], &err);
if (sock) {
sock_update_netprioidx(&sock->sk->sk_cgrp_data);
sock_update_classid(&sock->sk->sk_cgrp_data);
}
fd_install(new_fd, get_file(fp[i]));
}
…
/
- All of the files that fit in the message have had their
- usage counts incremented, so we just free the list.
*/
__scm_destroy(scm);
一旦文件描述符被安装,__scm_destroy(链接)清理分配的scm->fp并递减每个传输的文件结构的文件引用计数:
void __scm_destroy(struct scm_cookie *scm)
{
struct scm_fp_list *fpl = scm->fp;
int i;
if (fpl) {
scm->fp = NULL;
for (i=fpl->count-1; i>=0; i--)
fput(fpl->fp[i]);
free_uid(fpl->user);
kfree(fpl);
}
}
引用计数和Inflight计数
如上所述,当使用SCM_RIGHTS传递文件描述符时,其引用计数立即增加。一旦接收方套接字接受并安装了传递的文件描述符,引用计数就会减少。复杂性来自这个操作的“中间”状态:在文件描述符被发送之后,但在接收方接受并安装文件描述符之前。
让我们考虑以下场景:
- 进程创建套接字A和B。
- A将套接字A发送给套接字B。
- B将套接字B发送给套接字A。
- 关闭A。
- 关闭B。

引用计数循环的场景
两个套接字在接受传递的文件描述符之前都被关闭。A和B的引用计数都是1,并且无法进一步递减,因为它们在各自进程关闭时从内核fd表中移除。因此内核无法释放两个skb和sock结构,形成了一个不可打破的循环。Linux内核垃圾回收系统旨在防止这种特定场景下的内存耗尽。实现了inflight计数来识别潜在的垃圾。每次由于发送SCM_RIGHTS数据报而增加引用计数时,inflight计数也会增加。
当文件描述符通过SCM_RIGHTS数据报发送时,Linux内核将其unix_sock放入全局列表gc_inflight_list。内核增加unix_tot_inflight,它计算inflight套接字的总数。然后,内核增加u->inflight,它跟踪每个文件描述符在unix_inflight函数(scm.c#L45)中的inflight计数,该函数从unix_attach_fds调用:
void unix_inflight(struct user_struct *user, struct file *fp)
{
struct sock *s = unix_get_socket(fp);
spin_lock(&unix_gc_lock);
if (s) {
struct unix_sock *u = unix_sk(s);
if (atomic_long_inc_return(&u->inflight) == 1) {
BUG_ON(!list_empty(&u->link));
list_add_tail(&u->link, &gc_inflight_list);
} else {
BUG_ON(list_empty(&u->link));
}
unix_tot_inflight++;
}
user->unix_inflight++;
spin_unlock(&unix_gc_lock);
}
因此,当在套接字A和B之间传输文件描述符时,sk_buff看起来像这样:

A的inflight计数增加
当从另一侧接收套接字文件描述符时,unix_sock.inflight计数将减少。
让我们在close系统调用之前重新审视引用计数循环场景。这个循环是可打破的,因为任何套接字文件都可以接收传输的文件并打破引用循环:

关闭A和B之前的可打破循环
关闭两个文件描述符后,每个套接字文件描述符的引用计数等于其inflight计数,这是可能垃圾的标志:

关闭A和B后的不可打破循环
现在,让我们检查另一个示例。假设我们有套接字A、B和𝛼:
- A将套接字A发送给套接字B。
- B将套接字B发送给套接字A。
- B将套接字B发送给套接字𝛼。
- 𝛼将套接字𝛼发送给套接字B。
- 关闭A。
- 关闭B。

A、B和𝛼的可打破循环
这个循环是可打破的,因为我们可以从套接字文件描述符𝛼获取新安装的文件描述符B',并从B'获取新安装的文件描述符A'。
垃圾回收
lwn.net提供了垃圾回收的高级视图:
“如果两个计数相等,那么该文件结构可能是不可达循环的一部分。为了确定是否是这种情况,内核找到所有引用都包含在SCM_RIGHTS数据报中的飞行中Unix域套接字集合(换句话说,f_count和inflight相等)。然后计算这些套接字中有多少引用来自附加到此集合中套接字的SCM_RIGHTS数据报。任何有来自集合外部引用的套接字都是可达的,可以从集合中移除。如果它是可达的,并且有任何等待被使用的SCM_RIGHTS数据报附加到它,那么该数据报中包含的文件也是可达的,可以从集合中移除。
在迭代过程结束时,内核可能会发现自己有一个飞行中Unix域套接字集合,这些套接字仅由未使用(且不可使用)的SCM_RIGHTS数据报引用;此时,它有一个文件结构循环,这些结构持有彼此的唯一引用。从队列中移除这些数据报,释放它们持有的引用,并丢弃它们将打破循环。”
更具体地说,SCM_RIGHTS垃圾回收系统是为了处理不可打破的引用循环而开发的。为了识别哪些文件描述符是不可打破循环的一部分:
- 将引用计数等于其inflight计数的任何unix_sock对象添加到gc_candidates列表。
- 确定套接字是否被gc_candidates列表之外的任何套接字引用。如果是,那么它是可达的,将其及其引用的任何套接字从gc_candidates列表中移除。重复直到找不到更多可达套接字。
- 在此迭代过程之后,仅剩下那些仅被gc_candidates列表中的其他套接字引用的套接字。
让我们更仔细地看看这个垃圾回收过程是如何工作的。首先,内核找到所有引用计数等于其inflight计数的unix_sock对象,并将它们放入gc_candidates列表(garbage.c#L242):
list_for_each_entry_safe(u, next, &gc_inflight_list, link) {
long total_refs;
long inflight_refs;
total_refs = file_count(u->sk.sk_socket->file);
inflight_refs = atomic_long_read(&u->inflight);
BUG_ON(inflight_refs < 1);
BUG_ON(total_refs < inflight_refs);
if (total_refs == inflight_refs) {
list_move_tail(&u->link, &gc_candidates);
__set_bit(UNIX_GC_CANDIDATE, &u->gc_flags);
__set_bit(UNIX_GC_MAYBE_CYCLE, &u->gc_flags);
}
}
接下来,内核移除任何被当前gc_candidates列表之外的套接字引用的套接字。为此,内核调用scan_children(garbage.c#138)以及函数指针dec_inflight来遍历每个候选的sk->receive_queue。它减少每个传递的文件描述符的inflight计数,这些文件描述符本身也是垃圾回收的候选(garbage.c#L261):
/* Now remove all internal in-flight reference to children of
- the candidates.
*/
list_for_each_entry(u, &gc_candidates, link)
scan_children(&u->sk, dec_inflight, NULL);
遍历所有候选后,如果一个gc候选仍然有正的inflight计数,这意味着它被gc_candidates列表之外的对象引用,因此是可达的。这些候选不应包含在gc_candidates列表中,因此需要恢复相关的inflight计数。
为此,内核将候选放入not_cycle_list,并遍历gc_candidates列表中每个传输文件的接收队列(garbage.c#L281)并将inflight计数递增回来。整个过程递归完成,以便垃圾回收避免清除可达套接字:
/* Restore the references for children of all candidates,
- which have remaining references. Do this recursively, so
- only those remain, which form cyclic references.
- Use a "cursor" link, to make the list traversal safe, even
- though elements might be moved about.
/
list_add(&cursor, &gc_candidates);
while (cursor.next != &gc_candidates) {
u = list_entry(cursor.next, struct unix_sock, link);
/ Move cursor to after the current position. */
list_move(&cursor, &u->link);
if (atomic_long_read(&u->inflight) > 0) {
list_move_tail(&u->link, ¬_cycle_list);
__clear_bit(UNIX_GC_MAYBE_CYCLE, &u->gc_flags);
scan_children(&u->sk, inc_inflight_move_tail, NULL);
}
}
list_del(&cursor);
现在gc_candidates仅包含“垃圾”。内核从gc_candidates恢复原始inflight计数,将候选从not_cycle_list移回gc_inflight_list,并调用__skb_queue_purge清理垃圾(garbage.c#L306)。
/* Now gc_candidates contains only garbage. Restore original
- inflight counters for these as well, and remove the skbuffs
- which are creating the cycle(s).
/
skb_queue_head_init(&hitlist);
list_for_each_entry(u, &gc_candidates, link)
scan_children(&u->sk, inc_inflight, &hitlist);
/ not_cycle_list contains those sockets which do not make up a - cycle. Restore these to the inflight list.
/
while (!list_empty(¬_cycle_list)) {
u = list_entry(not_cycle_list.next, struct unix_sock, link);
__clear_bit(UNIX_GC_CANDIDATE, &u->gc_flags);
list_move_tail(&u->link, &gc_inflight_list);
}
spin_unlock(&unix_gc_lock);
/ Here we are. Hitlist is filled. Die. */
__skb_queue_purge(&hitlist);
spin_lock(&unix_gc_lock);
__skb_queue_purge清除接收队列中的每个skb:
/**
- __skb_queue_purge - empty a list
- @list: list to empty
- Delete all buffers on an &sk_buff list. Each buffer is removed from
- the list and one reference dropped. This function does not take the
- list lock and the caller must hold the relevant locks to use it.
*/
void skb_queue_purge(struct sk_buff_head *list);
static inline void __skb_queue_purge(struct sk_buff_head *list)
{
struct sk_buff *skb;
while ((skb = __skb_dequeue(list)) != NULL)
kfree_skb(skb);
}
有两种方法可以触发垃圾回收过程:
- 如果飞行中套接字超过16,000个,wait_for_unix_gc在sendmsg函数开头被调用
- 当套接字文件被内核释放时(即文件描述符关闭),内核将直接调用unix_gc。
注意unix_gc不是抢占式的。如果垃圾回收已经在进行中,内核不会执行另一个unix_gc调用。
现在,让我们用一对套接字f00和f01以及单个套接字𝛼检查这个示例(可打破循环):
- 套接字f00将套接字f00发送给套接字f01。
- 套接字f01将套接字f01发送给套接字𝛼。
- 关闭f00。
- 关闭f01。
在开始垃圾回收过程之前,套接字文件描述符的状态是:
- f00:ref = 1,inflight = 1
- f01:ref = 1,inflight = 1
- 𝛼:ref = 1,inflight = 0

由f00、f01和𝛼形成的可打破循环
在垃圾回收过程中,f00和f01被视为垃圾候选。f00的inflight计数降至零,但f01的计数仍为1,因为𝛼不是候选。因此,内核将从f01的接收队列恢复inflight计数。结果,f00和f01不再被视为垃圾。
CVE-2021-0920根本原因分析
当用户从recvmsg接收SCM_RIGHTS消息而没有MSG_PEEK标志时,如果垃圾回收过程正在进行,内核将等待其完成。但是,如果设置了MSG_PEEK标志,内核将增加传输文件结构的引用计数,而不与任何正在进行的垃圾回收过程同步。这可能导致内部垃圾回收状态不一致,使垃圾回收器将非垃圾sock对象标记为垃圾进行清除。
没有MSG_PEEK标志的recvmsg
内核函数unix_stream_read_generic(af_unix.c#L2290)解析SCM_RIGHTS消息并在未设置MSG_PEEK标志时管理文件inflight计数。然后,函数unix_stream_read_generic调用unix_detach_fds来减少inflight计数。然后,unix_detach_fds从skb清除传递的文件描述符列表(scm_fp_list):
static void unix_detach_fds(struct scm_cookie *scm, struct sk_buff *skb)
{
int i;
scm->fp = UNIXCB(skb).fp;
UNIXCB(skb).fp = NULL;
for (i = scm->fp->count-1; i >= 0; i--)
unix_notinflight(scm->fp->user, scm->fp->fp[i]);
}
unix_detach_fds中的unix_notinflight将通过减少inflight计数来反转unix_inflight的效果:
void unix_notinflight(struct user_struct *user, struct file *fp)
{
struct sock *s = unix_get_socket(fp);
spin_lock(&unix_gc_lock);
if (s) {
struct unix_sock *u = unix_sk(s);
BUG_ON(!atomic_long_read(&u->inflight));
BUG_ON(list_empty(&u->link));
if (atomic_long_dec_and_test(&u->inflight))
list_del_init(&u->link);
unix_tot_inflight--;
}
user->unix_inflight--;
spin_unlock(&unix_gc_lock);
}
稍后,skb_unlink和consume_skb从unix_stream_read_generic(af_unix.c#2451)调用以销毁当前skb。沿着调用链kfree(skb)->__kfree_skb,内核将调用函数指针skb->destructor(代码),它重定向到unix_destruct_scm:
static void unix_destruct_scm(struct sk_buff skb)
{
struct scm_cookie scm;
memset(&scm, 0, sizeof(scm));
scm.pid = UNIXCB(skb).pid;
if (UNIXCB(skb).fp)
unix_detach_fds(&scm, skb);
/ Alas, it calls VFS /
/ So fscking what? fput() had been SMP-safe since the last Summer */
scm_destroy(&scm);
sock_wfree(skb);
}
实际上,unix_detach_fds不会在这里从unix_destruct_scm再次调用,因为UNIXCB(skb).fp已经被unix_detach_fds清除。最后,调用scm_detach_fds中的fd_install(new_fd, get_file(fp[i]))来安装新的文件描述符。
带有MSG_PEEK标志的recvmsg
如果设置了MSG_PEEK标志,recvmsg过程则不同。MSG_PEEK标志在接收期间用于“窥视”消息,但数据被视为未读。unix_stream_read_generic将调用scm_fp_dup而不是unix_detach_fds。这增加了飞行中文件的引用计数(af_unix.c#2149):
/* It is questionable, see note in unix_dgram_recvmsg.
*/
if (UNIXCB(skb).fp)
scm.fp = scm_fp_dup(UNIXCB(skb).fp);
sk_peek_offset_fwd(sk, chunk);
if (UNIXCB(skb).fp)
break;
因为数据应被视为未读,所以当设置MSG_PEEK标志时,skb不会被取消链接和消耗。但是,接收方仍然会为飞行中套接字获取一个新的文件描述符。
recvmsg示例
让我们看一个具体示例。假设有以下套接字对:
- f00,f01
- f10,f11
现在,程序执行以下操作:
- f00 → [f00] → f01(意味着f00发送[f00]给f01)
- f10 → [f00] → f11
- 关闭(f00)

由f00、f01、f10和f11形成的可打破循环
状态如下:
- inflight(f00) = 2,ref(f00) = 2
- inflight(f01) = 0,ref(f01) = 1
- inflight(f10) = 0,ref(f10) = 1
- inflight(f11) = 0,ref(f11) = 1
如果现在发生垃圾回收过程,在任何recvmsg调用之前,内核将选择f00作为垃圾候选。但是,f00的inflight计数不会改变,内核不会清除任何垃圾。
如果f01然后调用带有MSG_PEEK标志的recvmsg,接收队列不会改变,inflight计数不会减少。f01获取一个新的文件描述符f00',它增加了f00的引用计数:

MSG_PEEK增加f00的引用计数,而接收队列未被清除
状态:
- inflight(f00) = 2,ref(f00) = 3
- inflight(f01) = 0,ref(f01) = 1
- inflight(f10) = 0,ref(f10) = 1
- inflight(f11) = 0,ref(f11) = 1
然后,f01调用没有MSG_PEEK标志的recvmsg,f01的接收队列被移除。f01还获取一个新的文件描述符f00'':

f01的接收队列被清除,f01''从f01获取
状态:
- inflight(f00) = 1,ref(f00) = 3
- inflight(f01) = 0,ref(f01) = 1
- inflight(f10) = 0,ref(f10) = 1
- inflight(f11) = 0,ref(f11) = 1
UAF场景
从非常高的层面来看,Linux垃圾回收的内部状态可能是非确定性的,因为MSG_PEEK不与垃圾回收器同步。存在一个竞争条件,垃圾回收器可以将飞行中套接字视为垃圾候选,同时在MSG_PEEK接收期间文件引用增加。因此,垃圾回收器可能清除候选,释放套接字缓冲区,而接收方可能安装文件描述符,导致skb对象上的UAF。
让我们看看捕获的0day样本如何逐步触发该漏洞(简化版本,实际上可能需要更多线程协同工作,但它应该展示核心思想)。首先,样本分配以下套接字对和单个套接字𝛼:
- f00,f01
- f10,f11
- f20,f21
- f30,f31
- 套接字𝛼(实际上可能有数千个𝛼来延长垃圾回收过程,以避免稍后将介绍的BUG_ON检查)。
现在,程序执行以下操作:

在任何recvmsg调用之前关闭以下文件描述符:
- 关闭(f00)
- 关闭(f01)
- 关闭(f11)
- 关闭(f10)
- 关闭(f30)
- 关闭(f31)
- 关闭(𝛼)
状态如下:
- inflight(f00) = N + 1,ref(f00) = N + 1
- inflight(f01) = 2,ref(f01) = 2
- inflight(f10) = 3,ref(f10) = 3
- inflight(f11) = 1,ref(f11) = 1
- inflight(f20) = 0,ref(f20) = 1
- inflight(f21) = 0,ref(f21) = 1
- inflight(f31) = 1,ref(f31) = 1
- inflight(𝛼) = 1,ref(𝛼) = 1
如果现在发生垃圾回收过程,内核将执行以下检查:
- 将f00、f01、f10、f11、f31、𝛼列为垃圾候选。减少每个接收队列中候选子项的inflight计数。
- 由于f21不被视为候选,f11的inflight计数仍高于零。
- 递归恢复inflight计数。
- 没有被视为垃圾。
可以通过以下方式触发潜在的skb UAF竞争条件:
从f21调用带有MSG_PEEK标志的recvmsg以获取f11'。
从f11调用带有MSG_PEEK标志的recvmsg以获取f10'。
并发执行以下操作:
从f11调用没有MSG_PEEK标志的recvmsg以获取f10''。
从f10'调用带有MSG_PEEK标志的recvmsg。
这怎么可能?让我们看一个没有触发竞争条件因此没有UAF的情况:
线程0
线程1
线程2
调用unix_gc
阶段0:将f00、f01、f10、f11、f31、𝛼列为垃圾候选。
从f21调用带有MSG_PEEK标志的recvmsg以获取f11'
增加引用计数:scm.fp = scm_fp_dup(UNIXCB(skb).fp);
阶段0:减少每个垃圾候选子项的inflight计数
阶段0后的状态:
inflight(f00) = 0
inflight(f01) = 0
inflight(f10) = 0
inflight(f11) = 1
inflight(f31) = 0
inflight(𝛼) = 0
阶段1:如果候选仍有inflight计数,则递归恢复inflight计数。
阶段1:所有inflight计数已恢复。
阶段2:没有垃圾,返回。
从f11调用带有MSG_PEEK标志的recvmsg以获取f10'
从f11调用没有MSG_PEEK标志的recvmsg以获取f10''
从f10'调用带有MSG_PEEK标志的recvmsg
皆大欢喜
皆大欢喜
皆大欢喜
但是,如果第二个recvmsg恰好在垃圾回收过程的阶段1之后发生,则触发UAF:
线程0
线程1
线程2
调用unix_gc
阶段0:将f00、f01、f10、f11、f31、𝛼列为垃圾候选。
从f21调用带有MSG_PEEK标志的recvmsg以获取f11'
增加引用计数:scm.fp = scm_fp_dup(UNIXCB(skb).fp);
阶段0:减少每个垃圾候选子项的inflight计数
阶段0后的状态:
inflight(f00) = 0
inflight(f01) = 0
inflight(f10) = 0
inflight(f11) = 1
inflight(f31) = 0
inflight(𝛼) = 0
阶段1:开始恢复inflight计数。
从f11调用带有MSG_PEEK标志的recvmsg以获取f10'
从f11调用没有MSG_PEEK标志的recvmsg以获取f10''
unix_detach_fds:UNIXCB(skb).fp = NULL
被spin_lock(&unix_gc_lock)阻塞
阶段1:scan_inflight无法从f11找到候选子项。因此,inflight计数意外保持不变。
阶段2:f00、f01、f10、f31、𝛼是垃圾。
阶段2:开始清除垃圾。
开始从f10'调用带有MSG_PEEK标志的recvmsg,期望接收f00'
获取skb = skb_peek(&sk->sk_receive_queue),skb将被线程0释放。
阶段2:对于
,稍后调用__skb_unlink和kfree_skb。
state->recv_actor(skb, skip, chunk, state) UAF
GC完成。
开始垃圾回收。
获取f10''
因此,竞争条件导致skb对象的UAF。乍一看,我们应该责怪第二个recvmsg系统调用,因为它清除了skb.fp,即传递的文件列表。但是,如果第一个recvmsg