获取中...

-

Just a minute...

sysmalloc用于初始化



当 _ int_malloc() 函数尝试从 fast bins , last remainder chunk , small bins , large bins 和 top chunk 都失败之后,就会使用 sYSMALLOc() 函数直接向系统申请内存用于分配所需的chunk。

0x01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
static void * sysmalloc(INTERNAL_SIZE_T nb, mstate av) {
mchunkptr old_top;
INTERNAL_SIZE_T old_size;
char *old_end;
long size;
char *brk;
long correction;
char *snd_brk;
INTERNAL_SIZE_T front_misalign;
INTERNAL_SIZE_T end_misalign;
char *aligned_brk;
mchunkptr p;
mchunkptr remainder;
unsigned long remainder_size;
size_t pagesize = GLRO(dl_pagesize);
bool tried_mmap = false;
...
old_top = av->top;
old_size = chunksize(old_top);
old_end = (char *) (chunk_at_offset(old_top, old_size));
brk = snd_brk = (char *) (MORECORE_FAILURE);
if (av != &main_arena) {
heap_info *old_heap, *heap;
size_t old_heap_size;
old_heap = heap_for_ptr(old_top);
old_heap_size = old_heap->size;
if ((long) (MINSIZE + nb - old_size) > 0
&& grow_heap(old_heap, MINSIZE + nb - old_size) == 0) {
av->system_mem += old_heap->size - old_heap_size;
arena_mem += old_heap->size - old_heap_size;
set_head(old_top,
(((char *) old_heap + old_heap->size) - (char *) old_top) | PREV_INUSE);
} else if ((heap = new_heap(nb + (MINSIZE + sizeof(*heap)), mp_.top_pad))) {
heap->ar_ptr = av;
heap->prev = old_heap;
av->system_mem += heap->size;
arena_mem += heap->size;
top (av) = chunk_at_offset(heap, sizeof(*heap));
set_head(top (av), (heap->size - sizeof (*heap)) | PREV_INUSE);
old_size = (old_size - MINSIZE ) & ~MALLOC_ALIGN_MASK;
set_head(chunk_at_offset (old_top, old_size + 2 * SIZE_SZ),
0 | PREV_INUSE);
if (old_size >= MINSIZE) {
set_head(chunk_at_offset (old_top, old_size),
(2 * SIZE_SZ) | PREV_INUSE);
set_foot(chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head(old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free(av, old_top, 1);
} else {
set_head(old_top, (old_size + 2 * SIZE_SZ) | PREV_INUSE);
set_foot(old_top, (old_size + 2 * SIZE_SZ));
}
} else if (!tried_mmap)
goto try_mmap;
}
else{
...
}
...
}

old_top、old_size和old_end分别保存了top chunk的指针,大小以及尾部的地址。如果是非主分配区,首先通过heap_for_ptr获得原top chunk对应的heap_info指针。

1
2
#define heap_for_ptr(ptr) \
((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1)))

对于非主分配区,因为每个heap是按照HEAP_MAX_SIZE的大小分配且对齐的,而每个topchunk存在于每个heap的剩余空间(高地址处),因此通过heap_for_ptr就能取出heap_info指针,heap_info保存了每个heap的相关信息。获得heap_info指针后,就能获得该heap当前被使用的大小并将其保存在old_heap_size中。当进入到sysmalloc前会尝试在top chunk分配内存,因此代码执行到这里肯定失败了。所以这里只有MINSIZE + nb - old_size>0这一种情况,即这时的top chunk空间不足了,因此首先通过grow_heap尝试向heap的高地址处增加heap当前使用的大小,就是top chunk的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int grow_heap(heap_info *h, long diff) {
size_t pagesize = GLRO(dl_pagesize);
long new_size;
diff = ALIGN_UP(diff, pagesize);
new_size = (long) h->size + diff;
if ((unsigned long) new_size > (unsigned long) HEAP_MAX_SIZE)
return -1;
if ((unsigned long) new_size > h->mprotect_size) {
if (__mprotect((char *) h + h->mprotect_size,
(unsigned long) new_size - h->mprotect_size,
PROT_READ | PROT_WRITE) != 0)
return -2;
h->mprotect_size = new_size;
}
h->size = new_size;
LIBC_PROBE(memory_heap_more, 2, h, h->size);
return 0;
}

h->size = new_size表示重新设置heap的大小至new_size。
在sysmalloc中,假设grow_heap成功,即将top chunk的大小设置为MINSIZE + nb,则重新设置分配区使用的内存大小,并且设置top chunk的size至新值(注意这里的size不能直接设置为MINSIZE + nb是因为在grow_heap中有对齐操作)。假如grow_heap失败,大部分情况下说明heap的使用大小已经接近其最大值HEAP_MAX_SIZE了,此时只能通过new_heap重新分配一个heap,注意传入的参数mp _ .top_pad表示在分配内存时,额外多分配的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
static heap_info * internal_function new_heap(size_t size, size_t top_pad) {
size_t pagesize = GLRO(dl_pagesize);
char *p1, *p2;
unsigned long ul;
heap_info *h;
if (size + top_pad < HEAP_MIN_SIZE)
size = HEAP_MIN_SIZE;
else if (size + top_pad <= HEAP_MAX_SIZE)
size += top_pad;
else if (size > HEAP_MAX_SIZE)
return 0;
else
size = HEAP_MAX_SIZE;
size = ALIGN_UP(size, pagesize);
p2 = MAP_FAILED;
if (aligned_heap_area) {
p2 = (char *) MMAP(aligned_heap_area, HEAP_MAX_SIZE, PROT_NONE,
MAP_NORESERVE);
aligned_heap_area = NULL;
if (p2 != MAP_FAILED && ((unsigned long) p2 & (HEAP_MAX_SIZE - 1))) {
__munmap(p2, HEAP_MAX_SIZE);
p2 = MAP_FAILED;
}
}
if (p2 == MAP_FAILED) {
p1 = (char *) MMAP(0, HEAP_MAX_SIZE << 1, PROT_NONE, MAP_NORESERVE);
if (p1 != MAP_FAILED) {
p2 = (char *) (((unsigned long) p1 + (HEAP_MAX_SIZE - 1))
& ~(HEAP_MAX_SIZE - 1));
ul = p2 - p1;
if (ul)
__munmap(p1, ul);
else
aligned_heap_area = p2 + HEAP_MAX_SIZE;
__munmap(p2 + HEAP_MAX_SIZE, HEAP_MAX_SIZE - ul);
} else {
p2 = (char *) MMAP(0, HEAP_MAX_SIZE, PROT_NONE, MAP_NORESERVE);
if (p2 == MAP_FAILED)
return 0;
if ((unsigned long) p2 & (HEAP_MAX_SIZE - 1)) {
__munmap(p2, HEAP_MAX_SIZE);
return 0;
}
}
}
if (__mprotect(p2, size, PROT_READ | PROT_WRITE) != 0) {
__munmap(p2, HEAP_MAX_SIZE);
return 0;
}
h = (heap_info *) p2;
h->size = size;
h->mprotect_size = size;
LIBC_PROBE(memory_heap_new, 2, h, h->size);
return h;
}

首先对需要分配的内存大小size做相应的调整。aligned_heap_area表示上一次MMAP分配后的结束地址,如果存在,就首先尝试从该地址分配大小为HEAP_MAX_SIZE的内存。MMAP最后是系统调用,对应的内核函数在《malloc源码分析—2》中已经介绍过了,这里只是一些标志位的区别。分配完后,会检查地址是否对齐,如果不对齐也是失败。
如果第一次分配失败了,就会再尝试一次,这次分配HEAP_MAX_SIZE * 2 大小的内存,并且新内存的起始地址由内核决定。因为尝试分配了HEAP _ MAX _ SIZE * 2大小的内存,其中必定包含了大小为HEAP_MAX_SIZE且和HEAP_MAX_SIZE对齐的内存,因此一旦分配成功,就从中截取出这部分内存。
如果连第二次也分配失败了,就会通过MMAP进行第三次分配,这次只分配HEAP_MAX_SIZE大小的内存,并且起始地址由内核决定,如果又失败了就返回0。
如果三面三次分配内存任何一次成功,就设置相应的可读写位置,并且返回分配区的heap_info指针。

重新回到sysmalloc中,假设分配成功,就会对刚刚分配得到的heap做相应的设置,其中ar_ptr表示所属的分配区的指针,prev表示上一个heap,所有的heap通过prev形成单向链表,然后通过set_head设置av分配区top chunk的size,这里也可以看出,对于刚分配的heap,包含了heap_info指针、top chunk、以及大于size的未被使用的部分。
再接下来就要对原来的top chunk进行最后的处理,这里假设对齐,如果原top chunk的大小不够大,就将其分割成old_size + 2 * SIZE_SZ和2 * SIZE_SZ大小;如果原top chunk的大小足够大,就将其分割成old_size,2 * SIZE_SZ和2 * SIZE_SZ大小,并通过 _ int_free进行释放。

0x02

针对主分配区的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static void * sysmalloc(INTERNAL_SIZE_T nb, mstate av) {
...
if (av != &main_arena) {
...
}
else{
size = nb + mp_.top_pad + MINSIZE;
if (contiguous(av))
size -= old_size;
size = ALIGN_UP(size, pagesize);
if (size > 0) {
brk = (char *) (MORECORE(size));
LIBC_PROBE (memory_sbrk_more, 2, brk, size);
}
if (brk != (char *) (MORECORE_FAILURE)) {
void (*hook)(void) = atomic_forced_read (__after_morecore_hook);
if (__builtin_expect (hook != NULL, 0))
(*hook)();
}
else{
if (contiguous (av))
size = ALIGN_UP (size + old_size, pagesize);
if ((unsigned long) (size) < (unsigned long) (MMAP_AS_MORECORE_SIZE))
size = MMAP_AS_MORECORE_SIZE;
if ((unsigned long) (size) > (unsigned long) (nb)){
char *mbrk = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0));
if (mbrk != MAP_FAILED){
brk = mbrk;
snd_brk = brk + size;
set_noncontiguous (av);
}
}
}
...
}
...
}

MORECORE是一个宏定义,其最终是通过系统调用分配内存,定义在linux内核的mmap.c文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
SYSCALL_DEFINE1(brk, unsigned long, brk){
unsigned long retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->mm;
unsigned long min_brk;
bool populate;
down_write(&mm->mmap_sem);
min_brk = mm->start_brk;
if (brk < min_brk)
goto out;
if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,
mm->end_data, mm->start_data))
goto out;
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
if (oldbrk == newbrk)
goto set_brk;
if (brk <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk))
goto set_brk;
goto out;
}
if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
goto out;
if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
goto out;
set_brk:
mm->brk = brk;
populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;
up_write(&mm->mmap_sem);
if (populate)
mm_populate(oldbrk, newbrk - oldbrk);
return brk;
out:
retval = mm->brk;
up_write(&mm->mmap_sem);
return retval;
}

首先会对传入堆的新地址brk做一些检查,然后该新地址小于原本的brk,就需要通过do_munmap释放虚拟内存,以减少堆的大小;反之,就通过do_brk增加堆得大小。其中find_vma_intersection用来判断增加堆空间后,是否会占用已经被分配的虚拟内存

1
2
3
4
5
6
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr){
struct vm_area_struct * vma = find_vma(mm,start_addr);
if (vma && end_addr <= vma->vm_start)
vma = NULL;
return vma;
}

因为是增加堆的大小,因此只需要关注do_brk函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static unsigned long do_brk(unsigned long addr, unsigned long len){
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
unsigned long flags;
struct rb_node **rb_link, *rb_parent;
pgoff_t pgoff = addr >> PAGE_SHIFT;
int error;
len = PAGE_ALIGN(len);
if (!len)
return addr;
flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;
error = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED);
if (error & ~PAGE_MASK)
return error;
error = mlock_future_check(mm, mm->def_flags, len);
if (error)
return error;
verify_mm_writelocked(mm);
while (find_vma_links(mm, addr, addr + len, &prev, &rb_link,
&rb_parent)) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
}
if (!may_expand_vm(mm, len >> PAGE_SHIFT))
return -ENOMEM;
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
if (security_vm_enough_memory_mm(mm, len >> PAGE_SHIFT))
return -ENOMEM;
vma = vma_merge(mm, prev, addr, addr + len, flags,
NULL, NULL, pgoff, NULL);
if (vma)
goto out;
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
vm_unacct_memory(len >> PAGE_SHIFT);
return -ENOMEM;
}
INIT_LIST_HEAD(&vma->anon_vma_chain);
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_pgoff = pgoff;
vma->vm_flags = flags;
vma->vm_page_prot = vm_get_page_prot(flags);
vma_link(mm, vma, prev, rb_link, rb_parent);
out:
perf_event_mmap(vma);
mm->total_vm += len >> PAGE_SHIFT;
if (flags & VM_LOCKED)
mm->locked_vm += (len >> PAGE_SHIFT);
vma->vm_flags |= VM_SOFTDIRTY;
return addr;
}

get_unmapped_area用来检查需要分配的虚拟内存地址是否已经被使用,find_vma_links用来查找需要插入的虚拟内存在红黑树的位置,may_expand_vm用来检查虚拟内存是否会超过系统的限制,vma_merge用来合并虚拟内存,如果不能合并,就通过slab分配一个vma,进行相应的设置,并通过vma_link插入到进程的红黑树中。

从linux的代码中回来,继续看sysmalloc,假设分配成功,会查找是否有 _ after_morecore_hook函数并执行,这里假设该函数指针为null。
假设分配失败,则进入else部分,首先对需要分配的大小按地址对齐,并且设置分配size的最小值为MMAP_AS_MORECORE_SIZE(1MB),然后通过MMAP宏分配内存,如果是通过mmap分配的内存,则设置分配区为不连续标志位。

0x03

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static void * sysmalloc(INTERNAL_SIZE_T nb, mstate av) {
...
if (av != &main_arena) {
...
}
else{
...
if (brk != (char *) (MORECORE_FAILURE)) {
if (mp_.sbrk_base == 0)
mp_.sbrk_base = brk;
av->system_mem += size;
if (brk == old_end && snd_brk == (char *) (MORECORE_FAILURE))
set_head(old_top, (size + old_size) | PREV_INUSE);
else if (contiguous (av) && old_size && brk < old_end) {
malloc_printerr(3, "break adjusted to free malloc space", brk, av);
}
else {
front_misalign = 0;
end_misalign = 0;
correction = 0;
aligned_brk = brk;
if (contiguous(av)) {
if (old_size)
av->system_mem += brk - old_end;
front_misalign = (INTERNAL_SIZE_T) chunk2mem(
brk) & MALLOC_ALIGN_MASK;
if (front_misalign > 0) {
correction = MALLOC_ALIGNMENT - front_misalign;
aligned_brk += correction;
}
correction += old_size;
end_misalign = (INTERNAL_SIZE_T) (brk + size + correction);
correction += (ALIGN_UP(end_misalign, pagesize)) - end_misalign;
assert(correction >= 0);
snd_brk = (char *) (MORECORE(correction));
if (snd_brk == (char *) (MORECORE_FAILURE)) {
correction = 0;
snd_brk = (char *) (MORECORE(0));
} else {
void (*hook)(
void) = atomic_forced_read (__after_morecore_hook);
if (__builtin_expect (hook != NULL, 0))
(*hook)();
}
}
...
}
}
}
...
}

假设增加了主分配区的top chunk成功,则更新sbrk_base和分配区已分配的内存大小。
然后,第一个判断表示,新分配的内存地址和原来的top chunk连续,并且不是通过MMAP分配的,这时只需要更新原来top chunk的大小size。
第二个判断表示如果分配区的连续标志位置位,top chunk的大小大于0,但是分配的brk小于原来的top chunk结束地址,这里就判定出错了。
进入第三个判断表示新分配的内存地址大于原来的top chunk的结束地址,但是不连续。这种情况下,如果分配区的连续标志位置位,则表示不是通过MMAP分配的,肯定有其他线程调用了brk在堆上分配了内存,av->system_mem += brk - old_end表示将其他线程分配的内存一并计入到该分配区分配的内存大小。然后将刚刚分配的地址brk按MALLOC_ALIGNMENT对齐。
再往下就要处理地址不连续的问题了,因为地址不连续,就要放弃原来top chunk后面一部分的内存大小,并且将这一部分内存大小“补上”到刚刚分配的新内存后面。首先计算堆上补上内存后的结束地址并保存在correction中,然后调用MORECORE继续分配一次,将新分配内存的开始地址保存在snd_brk中。如果分配失败,则将correction设为0,并将snd_brk重置为原来分配的内存的结束地址,表示放弃该次补偿操作;如果分配成功,就调用 __after_morecore_hook函数,这里假设该函数指针为null。

0x04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static void * sysmalloc(INTERNAL_SIZE_T nb, mstate av) {
...
if (av != &main_arena) {
...
}
else{
...
if (brk != (char *) (MORECORE_FAILURE)) {
...
if (brk == old_end && snd_brk == (char *) (MORECORE_FAILURE))
...
else if (contiguous (av) && old_size && brk < old_end) {
...
}
else {
...
if (contiguous(av)) {
...
}
else{
if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
assert (((unsigned long) chunk2mem (brk) & MALLOC_ALIGN_MASK) == 0);
else{
front_misalign = (INTERNAL_SIZE_T) chunk2mem (brk) & MALLOC_ALIGN_MASK;
if (front_misalign > 0){
aligned_brk += MALLOC_ALIGNMENT - front_misalign;
}
}
if (snd_brk == (char *) (MORECORE_FAILURE)){
snd_brk = (char *) (MORECORE (0));
}
}
if (snd_brk != (char *) (MORECORE_FAILURE)) {
av->top = (mchunkptr) aligned_brk;
set_head(av->top,
(snd_brk - aligned_brk + correction) | PREV_INUSE);
av->system_mem += correction;
if (old_size != 0) {
old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK;
set_head(old_top, old_size | PREV_INUSE);
chunk_at_offset (old_top, old_size)->size = (2 * SIZE_SZ)
| PREV_INUSE;
chunk_at_offset (old_top, old_size + 2 * SIZE_SZ)->size = (2
* SIZE_SZ) | PREV_INUSE;
if (old_size >= MINSIZE) {
_int_free(av, old_top, 1);
}
}
}
}
}
}
...
}

开头的else表示分配区的连续标志没有置位,这时只要按照MALLOC_ALIGNMENT做简单的对齐就行了,如果是通过brk分配的内存,则通过MORECORE (0)得到新分配的内存的结束地址并保存在snd_brk中。再往下进入if,设置分配区的top指针为经过对齐之后的起始地址aligned_brk,设置top chunk的大小size,aligned_brk表示对齐造成的误差,correction是因为要补偿原来top chunk剩余内存造成的误差,然后设置分配区已分配的内存大小。因为不连续,最后if内是设置原top chunk的fencepost,将原来top chunk的剩余空间拆成两个SIZE_SZ * 2大小的chunk,如果剩下的大小大于可分配的chunk的最小值MINSIZE,就通过 _ int_free释放掉整个剩余。

0x05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void * sysmalloc(INTERNAL_SIZE_T nb, mstate av) {
...
if ((unsigned long) av->system_mem > (unsigned long) (av->max_system_mem))
av->max_system_mem = av->system_mem;
check_malloc_state (av);
p = av->top;
size = chunksize(p);
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE )) {
remainder_size = size - nb;
remainder = chunk_at_offset(p, nb);
av->top = remainder;
set_head(p, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, p, nb);
return chunk2mem(p);
}
__set_errno(ENOMEM);
return 0;
}

这里就是获得前面所有代码更新后的top chunk,然后从该top chunk中分配用户需要的大小chunk并返回,如果失败则返回0

0x06

sysmalloc用于初始化。进入sysmalloc函数就表示top chunk的空间不够了。假设当前分配区不是主分配区,就通过grow_heap增加top chunk的空间,如果失败就通过new_heap重新分配一个heap,并将该分配区的top chunk指针指向新分配的heap的空闲内存。如果当前分配区是主分配区,首先会通过brk在堆上分配内存以增加top chunk的空间,如果失败再通过MMAP分配。假设新分配内存的地址不连续,而分配区的连续标志位置位,就会继续分配内存以补偿。最后,只要分配成功,就可以从被更新的top chunk分配所需的内存。

相关文章
评论
分享
  • Alloc to Stack&Arbitary Alloc

    Alloc to Stack和Arbitary Alloc都利用了fastbin链表的特性。 Alloc To Stack利用了fastbin链表的特性。当前的chunk的fd指向下一个chunk。Alloc To Stack核心...

    Alloc to Stack&Arbitary Alloc
  • House of Spirit

    House of Spirit针对fastbin,也是fastbin attach的一种。核心在于在目标位置处伪造 fastbin chunk,并将其释放,从而达到分配指定地址的 chunk 的目的。 原理House of Spi...

    House of Spirit
  • Fastbin Double Free

    double free 是任意地址写的一种技巧,指堆上的某块内存被释放后,并没有将指向该堆块的指针清零,那么,我们就可以利用程序的其他部分对该内存进行再次的free, 利用条件Fastbin Double Free 能够成功利用主...

    Fastbin Double Free
Please check the parameter of comment in config.yml of hexo-theme-Annie!