红联Linux门户
Linux帮助

CentOS 5 rpcsec_gss_krb5模块卸载再重加载后导致kernel panic原因分析

发布时间:2008-04-11 16:14:51来源:红联作者:erfcend
在项目开发过程当中,发现CentOS 5当中,若对rpcsec_gss_krb5模块加载、卸载后再加载将导致kernel panic。通过对panic后Oops信息的分析可知panic位置处于sunrpc.ko模块auth_domain_put函数。这一函数在多个模块当中被调用,应该不至于出现严重问题,遂分析调用者auth_rpcgss模块中的svcauth_gss_register_pseudoflavor函数:

引用:
int
svcauth_gss_register_pseudoflavor(u32 pseudoflavor, char * name)
{
struct gss_domain *new;
struct auth_domain *test;
int stat = -ENOMEM;

new = kmalloc(sizeof(*new), GFP_KERNEL);
if (!new)
goto out;
kref_init(&new->h.ref);
new->h.name = kmalloc(strlen(name) + 1, GFP_KERNEL);
if (!new->h.name)
goto out_free_dom;
strcpy(new->h.name, name);
new->h.flavour = &svcauthops_gss;
new->pseudoflavor = pseudoflavor;

test = auth_domain_lookup(name, &new->h);
if (test != &new->h) { /* XXX Duplicate registration? */
auth_domain_put(&new->h); // <<<<---------- kernel panic here
/* dangling ref-count... */
goto out;
}
return 0;

out_free_dom:
kfree(new);
out:
return stat;
}


注意到几个特征:
1、rpcsec_gss_krb5模块是在再加载的过程当中才发生kernel panic的,于是这个panic可能与卸载模块工作不到位有关

2、panic的位置,处于auth_domain_lookup之后,从代码上下文和注释当中都可以看到调用auth_domain_put是为了避免重复注册,而对后注册的信息进行删除

查看代码有这样的发现,首先第一次加载模块时,auth_domain_lookup通过对name进行哈希运算后发现new->h当中的地址并未在哈希表当中出现,于是将其插入表中,并将传入的new->h值返回。若第二次调用auth_domain_lookup使用相同的名字,不同的new->h值时,将返回哈希表中的值,而不是传入的new->h值,所以后续代码可以通过此检测是否有重复name的注册。不幸的是,rpcsec_gss_krb5模块在加载的时候注册了若干的name,但是却没有在卸载的时候从哈希表当中删除对应的表项,使得第二次加载模块时会发现有重复的表项出现。当然,这只是kernel panic的诱因,而不是直接原因。

直接导致kernel panic的原因在auth_domain_put代码当中:

引用:
void auth_domain_put(struct auth_domain *dom)
{
if (atomic_dec_and_lock(&dom->ref.refcount, &auth_domain_lock)) {
hlist_del(&dom->hash); // <<<----------- looking here
dom->flavour->domain_release(dom);
}
}


其在对传入的new->h信息进行了hlist_del操作,而hlist_del的函数如下,并调用了__hlist_del(n):

引用:
static inline void hlist_del(struct hlist_node *n)
{
__hlist_del(n); // <<-------- pay attention
n->next = LIST_POISON1;
n->pprev = LIST_POISON2;
}

static inline void __hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next;
struct hlist_node **pprev = n->pprev; // <<--------- pprev get a wrong value
*pprev = next; // <<-------- use this wrong pointer will cause unknow result
if (next)
next->pprev = pprev;
}


在解释上面代码之前,请回头看看svcauth_gss_register_pseudoflavor函数的代码,其对new->h的操作前后是先通过kmalloc为new分配了空间,而对new->h当中的结构体内容内部赋值了name,flavour,而对ref和hash没有进行操作,其希望是通过函数auth_domain_lookup来完成的:

引用:
struct auth_domain *
auth_domain_lookup(char *name, struct auth_domain *new)
{
struct auth_domain *hp;
struct hlist_head *head;
struct hlist_node *np;

head = &auth_domain_table[hash_str(name, DN_HASHBITS)];

spin_lock(&auth_domain_lock);

hlist_for_each_entry(hp, np, head, hash) {
if (strcmp(hp->name, name)==0) {
kref_get(&hp->ref);
spin_unlock(&auth_domain_lock);
return hp; // <<---------- if name is duplicated, no operation to the new parameter
}
}
if (new) { // <<---------- if name is fresh for the list, run here
hlist_add_head(&new->hash, head);
kref_get(&new->ref);
}
spin_unlock(&auth_domain_lock);
return new;
}


当然,很不幸的是,auth_domain_lookup函数只有在当name没有注册的时候才会对ref和hash成员进行赋值,而当有重复的name时,后传入的auth_domain结构完全没有被改变。于是在svcauth_gss_register_pseudoflavor函数当中,调用auth_domain_put时所操作的auth_domain结构体中的ref和hash成员都是kmalloc后未初始化的值,导致__hlist_del函数引用出错。

我本想试着修正这个问题,不过现在看来牵扯比较大,只能暂时作罢。
文章评论

共有 0 条评论