Linux Debug object

Table of Contents

功能介绍

Debug Object 是内核中为了监控对象生命周期而引入的检测机制,通过该机制,系统会
在对象发生不正常状态切换时报出来错误。

状态列表

none
未初始化
initialized
已初始化
inactive
初始化,但未激活
active
初始化,已激活
destroyed
已销毁
not available
未找到对象

状态流转图

下图是debug object的状态流转图,下述流程之外的状态是不备被允许的

debug-object-stats.png

API

头文件如下:

#include <linux/debugobjects.h>

debugobjects的 API 比较简单,在此不做解释。

实例

debugobjects对于static object处理的BUG

syzbot link: WARNING in __mod_timer

Log 如下:

------------[ cut here ]------------
ODEBUG: assert_init not available (active state 0) object: ffffffff8d4fcbc0 object type: timer_list hint: key_gc_timer_func+0x0/0x80 security/keys/gc.c:117
WARNING: CPU: 1 PID: 10646 at lib/debugobjects.c:512 debug_print_object lib/debugobjects.c:509 [inline]
WARNING: CPU: 1 PID: 10646 at lib/debugobjects.c:512 debug_object_assert_init+0x1f2/0x240 lib/debugobjects.c:899
Modules linked in:
CPU: 1 PID: 10646 Comm: syz-executor.3 Not tainted 6.2.0-syzkaller-06695-gd8ca6dbb8de7 #0
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 01/21/2023
RIP: 0010:debug_print_object lib/debugobjects.c:509 [inline]
RIP: 0010:debug_object_assert_init+0x1f2/0x240 lib/debugobjects.c:899
Code: a0 fd 4c 8b 4d 00 48 c7 c7 20 68 38 8b 48 c7 c6 00 65 38 8b 48 c7 c2 e0 69 38 8b 31 c9 4d 89 e8 53 e8 42 6f 10 fd 48 83 c4 08 <0f> 0b ff 05 42 e1 10 0a 48 83 c5 38 48 89 e8 48 c1 e8 03 42 80 3c
RSP: 0018:ffffc9000b1276f8 EFLAGS: 00010282
RAX: caefb79dfa053800 RBX: ffffffff83f63700 RCX: 0000000000040000
RDX: ffffc90014421000 RSI: 0000000000018ba9 RDI: 0000000000018baa
RBP: ffffffff8aec5400 R08: ffffffff81536d42 R09: fffff52001624e55
R10: 0000000000000000 R11: dffffc0000000001 R12: dffffc0000000000
R13: ffffffff8d4fcbc0 R14: 0000000000000004 R15: ffffffff91d3e4e8
FS:  00007f122cc29700(0000) GS:ffff8880b9900000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 000055555632f708 CR3: 00000000690df000 CR4: 00000000003526e0
DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
Call Trace:
 <TASK>
 debug_timer_assert_init kernel/time/timer.c:792 [inline]
 debug_assert_init kernel/time/timer.c:837 [inline]
 __mod_timer+0x10d/0xf40 kernel/time/timer.c:1020
 key_reject_and_link+0x3f5/0x6e0 security/keys/key.c:610
 key_negate_and_link include/linux/key-type.h:187 [inline]
 complete_request_key security/keys/request_key.c:64 [inline]
 call_sbin_request_key+0xa7b/0xcd0 security/keys/request_key.c:213
 construct_key security/keys/request_key.c:244 [inline]
 construct_key_and_link security/keys/request_key.c:503 [inline]
 request_key_and_link+0x11e3/0x18e0 security/keys/request_key.c:637
 __do_sys_request_key security/keys/keyctl.c:222 [inline]
 __se_sys_request_key+0x271/0x3b0 security/keys/keyctl.c:167
 do_syscall_x64 arch/x86/entry/common.c:50 [inline]
 do_syscall_64+0x41/0xc0 arch/x86/entry/common.c:80
 entry_SYSCALL_64_after_hwframe+0x63/0xcd

ODEBUG: assert_init not available (active state 0) object: ffffffff8d4fcbc0 object type: timer_list hint:

由上面面log可知,Linux的密钥管理模块在调用 mod_timer 时发现,这个timer对象未初始化,所以触发了assert的检查。

原因分析

调用路径:
key_schedule_gc(key->expiry + key_gc_delay);
mod_timer(&key_gc_timer, expires);

这里的 key_gc_timer 是个全局静态定义的timer对象, 对于静态定义的timer对象, 是不
需要显式调用 timer_setup 等初始化函数来进行初始化的。

/*
 * Reaper for links from keyrings to dead keys.
 */
static void key_gc_timer_func(struct timer_list *);
static DEFINE_TIMER(key_gc_timer, key_gc_timer_func);

对于 debug_object_assert_init 函数,

/**
 * debug_object_assert_init - debug checks when object should be init-ed
 * @addr:       address of the object
 * @descr:      pointer to an object specific debug description structure
 */
void debug_object_assert_init(void *addr, const struct debug_obj_descr *descr)
{
    struct debug_bucket *db;
    struct debug_obj *obj;
    unsigned long flags;

    if (!debug_objects_enabled)
        return;

    db = get_bucket((unsigned long) addr);

    raw_spin_lock_irqsave(&db->lock, flags);

    obj = lookup_object(addr, db);
    if (!obj) {
        struct debug_obj o = { .object = addr,
                       .state = ODEBUG_STATE_NOTAVAILABLE,
                       .descr = descr };

        raw_spin_unlock_irqrestore(&db->lock, flags);
        /*
         * Maybe the object is static, and we let the type specific
         * code confirm. Track this static object if true, else invoke
         * fixup.
         */
        if (descr->is_static_object && descr->is_static_object(addr)) {
            /* Track this static object */
            debug_object_init(addr, descr);
        } else {
            debug_print_object(&o, "assert_init");
            debug_object_fixup(descr->fixup_assert_init, addr,
                       ODEBUG_STATE_NOTAVAILABLE);
        }
        return;
    }

    raw_spin_unlock_irqrestore(&db->lock, flags);
}
EXPORT_SYMBOL_GPL(debug_object_assert_init);

上面对于static的处理是有判断的,对于静态数据,允许在assert_init时,直接将对应的debugobject进行初始化。

但是在我们的例子中,代码神奇的跑到了else分支,并没有将这个timer当作静态对象来处理。

为什么没有当作静态对象处理?

先看看判断静态对象的代码:

#define __TIMER_INITIALIZER(_function, _flags) {        \
        .entry = { .next = TIMER_ENTRY_STATIC },        \
        .function = (_function),                        \
        .flags = (_flags),                              \
        __TIMER_LOCKDEP_MAP_INITIALIZER(                \
            __FILE__ ":" __stringify(__LINE__))         \
    }

#define DEFINE_TIMER(_name, _function)          \
    struct timer_list _name =                   \
        __TIMER_INITIALIZER(_function, 0)

static bool timer_is_static_object(void *addr)
{
    struct timer_list *timer = addr;

    return (timer->entry.pprev == NULL &&
        timer->entry.next == TIMER_ENTRY_STATIC);
}

从上面的代码可以看到,系统是通过 timer->entry 的值来知道此对象是一个静态对象的。
这样就很容易联想到,如果timer被加入系统的timer list中,那么这个timer对象的
timer->entry 的值就和初始值不一样了,所以如果timer被使用之后,这个判断就不成立
了。

What? 是不是发现了个大BUG,为什么syzbot才报了这么一次?

经过分析,这个问题其实并不严重,实际上,对于静态对象,只有在调用 debug_object_activate 和 debug_object_assert_init 时才有特殊处理。其他地方没有相关的处理。

对于 debug_object_activate, 只要不是首次调用,系统都可以找到已经申请到的
debugobject对象,从而可以正确的来处理这个对象的状态。

对于 debug_object_assert_init, 也是相同的,只有在首次调时,依赖于对象的状态。所
以来言,只有在对象第一次使用时会有概率出现问题,而静态对象,一般不存在第一次使用
就出现并发的场景,这个出现问题的情景就比较少见。

race在哪里?

我的patch comments已经对现象的现场做了分析。

Link: https://lore.kernel.org/all/20230303161906.831686-1-schspa@gmail.com/

T0                                                   T1
=========================================================================
mod_timer();
  debug_object_assert_init
    db = get_bucket((unsigned long) addr);
    raw_spin_lock_irqsave(&db->lock, flags);
    obj = lookup_object(addr, db);
    if (!obj) {
        raw_spin_unlock_irqrestore(&db->lock, flags);
        << Context switch >>
                                             mod_timer();
                                               debug_object_assert_init
                                               ...
                                               enqueue_timer();
        /*
         * The initial state changed a static timer object, and
         * is_static_object will return false
         */

        if (descr->is_static_object &&
            descr->is_static_object(addr)) {
                debug_object_init();
            } else {
               << Hit here for a static object >>
               debug_print_object(&o, "assert_init");
               debug_object_fixup(descr->fixup_assert_init, addr,
                       ODEBUG_STATE_NOTAVAILABLE);
            }
    }

上面的场景描述了问题发现的场景,从地址来获取debugobject对象这个地方,实际上都有
&db->lock 来保护,而 is_static_object 的状态判断实际上依赖于对象的初始状态,
所以,事实上 is_static_object 也应该在系统第一次初始化的时候来判断, 而
&db->lock 刚好可以用来保证第一次申请对象的时候再调用 is_static_object 的接口。

修复

V1

  • 解决思路
    1. 添加额外的状态,并保证在 &db->lock 锁范围内,在对象首次初始化的时候调用
      is_static_object 接口,并将结果保存在debugobject对象中去。
    2. 在判断静态对象的时候,如果已经找到对象,就直接返回之间保存的静态对象状态位,
      而不需要调用 is_static_object
  • 存在的问题
    1. debug_objects_selftest 中将一个全局变量从不是static的类型,变为了static类型,
      这样,由于debugobject对象已经找到,此时将不会调用 is_static_object 接口,
      从而,事实上,未能将期转变为static对象,因此需要稍微修改一下目前的测试case。

Link: https://lore.kernel.org/all/20230303161906.831686-1-schspa@gmail.com/

maintainer version

  • 解决思路
    1. 由于出现问题是由于查找对象和初始化对象是在两次单独的临界区,因此造成了状态
      不一致,所以将这两个过程合并在一个临界去中完成,从而完成BUG修复。

Link: https://lore.kernel.org/all/87zg7dzgao.ffs@tglx/

Contact me via :)
虚怀乃若谷,水深则流缓。