Large Pool 错误

引用注明>> 【作者:张佩】【原文:www.yiiyee.cn/blog

我前几个月曾经分析了一个BAD_POOL_CALLER的问题(链接),今天收到的这个dump文件,系统是Win7 X64,最后发现问题和前者非常相似,但二者的分析过程却截然不同。

引子

打开dump文件后,首先进行自动分析。蓝屏号是0x4E。

************************************************************
*                                                          *
*                        Bugcheck Analysis                 *
*                                                          *
************************************************************

PFN_LIST_CORRUPT (4e)
Typically caused by drivers passing bad memory descriptor lists (ie: calling
MmUnlockPages twice with the same list, etc).  If a kernel debugger is
available get the stack trace.
Arguments:
Arg1: 000000000000009a, 
Arg2: 000000000014e26c
Arg3: 0000000000000006
Arg4: 0000000000000002

Debugging Details:
------------------

BUGCHECK_STR:  0x4E_9a

DEFAULT_BUCKET_ID:  WIN7_DRIVER_FAULT

PROCESS_NAME:  System

CURRENT_IRQL:  0

LAST_CONTROL_TRANSFER:  from fffff8000495b9ef to fffff800048ccbc0

STACK_TEXT:
... nt!KeBugCheckEx
... nt!MiBadRefCount+0x4f
... nt!MiFreePoolPages+0xa8b
... nt!ExFreePoolWithTag+0x7c7
... TestModule!MemMgr::Free+0x77 
//省略若干TestModule的调用帧
... nt!PopIrpWorker+0x3c5
... nt!PspSystemThreadStartup+0x5a
... nt!KxStartSystemThread+0x16

从Windbg的帮助文档中,找到0x4E相关的内容,根据第一个参数值0x9A,拿到下面的一些信息。

Parameter 1

Parameter 2

Parameter 3

Parameter 4

Cause of Error

0x9A Page frame number Current page state The reference count of the entry that is being removed A driver attempted to free a page that is still locked for IO.

驱动试图释放一个仍然被锁定的页。到目前为止,我对这个描述还是不太清楚。

内存分析

从调用栈看,出问题的时候,驱动程序正在调用ExFreePoolWithTag来释放一个内存块。加载符号文件,通过进一步分析,我发现正在释放的是一个维度为4的指针数组里面的第三个成员指针所指向的内存。前面两个指针所指向的内存已经释放成功,并且指针已经清零。剩下的两个指针值如下:

Pointer 2 = 0xfffffa80`04c6c000
pointer 3 = 0xfffffa80`04c8d000

分别对这两个指针使用!pool命令进行分析。如果!pool命令还能够认识它们,说明内存块相关的结构体没有被破坏。否则就是内存块被破坏了。

0: kd> !pool 0xfffffa80`04c6c000
Pool page fffffa8004c6c000 region is Nonpaged pool
fffffa8004c6c000 is not a valid large pool allocation, checking large session pool...
fffffa8004c6c000 is not a valid small pool allocation, checking large pool...
unable to get pool big page table - either wrong symbols or pool tagging is disabled
fffffa8004c6c000 is freed (or corrupt) pool
Bad allocation size @fffffa8004c6c000, zero is invalid

***
*** An error (or corruption) in the pool was detected;
*** Attempting to diagnose the problem.
***
*** Use !poolval fffffa8004c6c000 for more details.

Pool page [ fffffa8004c6c000 ] is __inVALID.

Analyzing linked list...
[ fffffa8004c6c000 --> fffffa8004c6c010 (size = 0x10 bytes)]: Corrupt region

Scanning for single bit errors...

None found

0: kd> !pool 0xfffffa80`04c8d000
Pool page fffffa8004c8d000 region is Nonpaged pool
*fffffa8004c8d000 : large page allocation, Tag is TAG6, size is 0x4020 bytes

两个指针的分析结果,第一个分析不出任何结果,第二个则看上去是正确的。为了确定第二个不是“碰巧”正确的,可确认一下TAG6这个内存分配时指定的tag,如果确实有的话,就基本可认为不是“碰巧”的了。

第一个内存块已经被破坏了,并且这时候,我已经确认,指针列表中的4个内存缓冲区的size都是一样的。所以知道第一个pool块的size也是0x4020字节。

我此时首先想到的是,为什么4个pool memory,看上去只有第三个被破坏了。因为前面两个已经被成功释放掉了,第四个也是好的。这有点奇怪。在没有进一步的资料前,我只能认为这4个pool memory是用在不同的地方,而第三个是碰巧被破坏了。后来的事实也证明,通过这个途径我不可能找到任何突破口。好在很快也就放弃了。

Large Pool

上一次我通过使用POOL_HEADER结构体来查看被破坏的pool块。但这一次,这个办法不能用。原因是这次内存块的大小超过了一个页面长度(定义为PAGE_SIZE,即4K字节)。

根据系统的定义(见MSDN),如果申请的内存块size < PAGE_SIZE,系统会在一个内存页中分配,不会跨越页边界。举例说,如果有一个0x20字节的内存申请请求,系统不会把page1尾巴上的0x10字节和page2前面的0x10字节拼在一起返回给申请者;只会把page2前面的0x20字节返回给调用者。

如果申请的size > PAGE_SIZE,系统怎么做的呢?假设现在用户准备申请5000字节(4K+4)的内存块,它恰好比一个page多4个字节。系统会以4K为边界,找到相邻的两个空闲页,将起始地址返回给申请者。PAGE1的全部和PAGE2的前面部分,由申请者使用;PAGE2尾部的部分,系统另做他用。

在这里,我们把这种大内存块称为:Large Pool

和普通pool块不同,large pool块不使用POOL_HEADER结构体来维护内存块的大小。所以这次我不能从POOL_HEADER入手,这正是困难所在。通过网上搜索,我发现了MSDN BLOG上的这篇文章。它介绍了一种查看large pool块完整性的方法。

在large pool块的尾部,系统会保留两个小pool块,并把它们的tag分别设置为Frag和Free。我估计frag是指Fragment(碎片),而Free指空闲的意思。

通过这篇文章介绍的方法,我先对正确的那个large pool块进行了测试:

0: kd> dc fffffa80`04c8d000+0x4020 L40
fffffa80`04c91020  02010000 67617246 00000000 00000000  ....Frag........
fffffa80`04c91030  00020001 65657246 00000000 00000000  ....Free........
fffffa80`04c91040  04c94040 fffffa80 04bf6040 fffffa80  @@......@`......
fffffa80`04c91050  020c0002 6c734d46 00000000 00000000  ....FMsl........

果然看到了Frag和Free这两个tag。能正确看到这两个tag,至少说明这个pool块在使用时没有发生越界的情况,否则应该会覆盖tag值(但如果只越界4个字节,也有可能,此处不考虑此种情况)。还可以对这个页使用!pool命令:

0: kd> !pool fffffa80`04c8d000+0x4020
Pool page fffffa8004c91020 region is Nonpaged pool
*fffffa8004c91020 size:   10 previous size:    0  (Allocated) *Frag
fffffa8004c91030 size:   20 previous size:   10  (Free)       Free
fffffa8004c91050 size:   c0 previous size:   20  (Allocated)  FMsl
fffffa8004c91110 size:  160 previous size:   c0  (Allocated)  Ntfx
后省略

说明这一页的前0x20个字节是large pool块的一部分。剩下的部分则可以看到开头tag为Frag和Free的特殊pool块。后面的页内存被其它模块申请。

再来看看发生错误的那个large pool块:

0: kd> dc fffffa8004c6c000+0x4020
fffffa80`04c70020  02010000 67617246 b660fdb5 4fabc845  ....Frag..`.E..O
fffffa80`04c70030  0c050003 6873534b 0538ab30 fffffa80  ....KSsh0.8.....
fffffa80`04c70040  053f6170 fffffa80 00000000 00000000  pa?.............
fffffa80`04c70050  00000001 00000001 00000000 00000000  ................
fffffa80`04c70060  000006e4 00000000 001ea6c8 00000000  ................
fffffa80`04c70070  00000000 00000000 00000032 00000001  ........2.......

0: kd> !pool fffffa8004c6c000+0x4000
Pool page fffffa8004c70000 region is Nonpaged pool
*fffffa8004c70000 size:   30 previous size:    0  (Free)      *Free
fffffa8004c70030 size:   50 previous size:   30  (Free )  KSsh Process: fffffa800538ab30
fffffa8004c70080 size:   80 previous size:   50  (Free )  Even (Protected)
fffffa8004c70100 size:  1a0 previous size:   80  (Free)       None
fffffa8004c702a0 size:   50 previous size:  1a0  (Free )  VadS
fffffa8004c702f0 size:   c0 previous size:   50  (Allocated)  FMsl
fffffa8004c703b0 size:   c0 previous size:   c0  (Free)       CcPL

可以看到被破坏的痕迹:Frag后面没有Free这个tag,而是tag为KSsh的pool块。这说明large pool 块被破坏了。而最可能的破坏原由就是缓冲区溢出。

后记

这时候,还有一个较好的办法可证实是不是缓冲区溢出:把指定pool块的长度增大后,再测试。我建议把申请的size增加到0x5000字节,其它地方不动。代码逻辑仍然把它当做只有0x4020字节来用。这个问题原本比较容易做出来,但做了这样的改动后,经过两天的测试,再也没有蓝屏。从侧面证实了缓冲区溢出的可能。对我而言,问题至此就算解决了。

另外还有两个悬疑。第一是为什么这个问题被报成0x4E错误,即试图释放被锁定的内存。我个人的理解是,系统在释放pool块时,pool所在的page已被其它模块锁定或他用,系统坚持释放而导致错误。

第二个问题是,系统既然不使用POOL_HEADER结构体,到底是如何维护large pool块的?我在网上查找large page/large pool关键字,暂时还没有得到满意的结果。

参考:

Stop 0x19 in a Large Pool Allocation
MSDN: ExAllocatePoolWithTag

8,746 total views, 6 views today

《Large Pool 错误》有6个想法

  1. 第一个内存块已经被破坏了,并且这时候,我已经确认,指针列表中的4个内存缓冲区的size都是一样的。所以第一个pool块的size也是0×4020字节。

    张老师,这是通过驱动代码确认的吗?

  2. 最可能的破坏原由就是缓冲区溢出

    有一点疑问,溢出的同时,"FRAG"这个TAG却又仍然存在.却把后面的”FREE”TAG给改掉了.

    1. 具体原因,要弄明白系统是怎么维护Large pool的,以及Free内存块的使用。这一点,我还找不到清晰的资料。

Wesley进行回复 取消回复

电子邮件地址不会被公开。