[W05] Challenge 04 - Double Free

silver ·2일 전

크래프톤 정글

목록 보기
18/19

- 문제

- 시나리오

직원 레코드(Rec)를 힙에 만들고, "ID 인덱스"(by_id)와 "이름 순 인덱스"(by_name) 두 배열로 관리한다.
두 인덱스는 정렬만 다를 뿐, 결국 "같은 Rec 객체들"을 가리킨다(별칭, aliasing).
조회/출력 후 프로그램 끝에서 전부 정리한다.

- 문제 풀이

이름부터 더블프리지만!!
일단 gdb ㄱㄱ

1) 어디서 터지는지 확인

일단 돌려보니 SIGABRT로 죽어서 bt부터 찍어봄

free(): double free detected in tcache 2
Program received signal SIGABRT, Aborted.
  • #6: 더블프리 탐지됨 확인
  • #8: 이때 free하려던 주소가 0x5555555592e0
  • #9, #10: main() → directory_free() 108번 줄에서 터짐

SIGABRT 상태에서 n을 더 치면 프로세스가 죽어버려서 d 같은 지역변수를 못 봄..
그래서 일단 bt로 어디에서 터졌는지 확인하고, 실제 변수 값은 프로그램이 살아있을 때 확인하기로 함

2)

directory_free에 브레이크 찍고 들어가봄

for (i...) {                       // 루프 1
    free(d->by_id[i]->name);
    free(d->by_id[i]);             // ← Rec 4개를 전부 free
}
for (i...) {                       // 루프 2
    free(d->by_name[i]);           // ← 같은 Rec 4개를 또 free → 터짐
}

by_id[i],by_name[i] → 같은 rec 객체를 가리킴
루프 1에서 전부 free하면 루프2의 by_name[i] 는 이미 프리된 포인터인데 같은 주소를 다시 free()하고 있음

실제로 같은 객체를 가리키는지 확인하기 위해 @4를 사용해서 배열의 주소를 한 번에 4개씩 확인

순서만 다르고 주소 4개가 똑같음 확인
by_id: carol → alice → dave → bob
by_name: alice → bob → carol → dave

3) 터지기 직전에 "이미 free된 애"인지 확인


1. display d->by_name[0]0x5555555592e0, 곧 free할 주소 가리킴
2. tcachebins → 해당 주소가 이미 tcache 리스트 안에 있음
3. x/4gx 0x5555555592e0 → 원래 Rec의 데이터가 있던 자리에 free() 이후 tcache에서 사용하는 값이 남아 있는 것을 확인

참고로 tcache는 한 bin에 최대 7개의 청크가 들어가기 때문에, 8번째로 해제된 Rec인 bob(0x555555559360)은 tcache가 아닌 fastbin으로 넘어간다. 그래서 tcachebins에는 7개만 보인다.

4) 그래서 누가 원본(owner)임?

static void directory_add(Directory *d, int id, const char *name) {
    Rec *r = rec_new(id, name);
    d->by_id[d->count]   = r;
    d->by_name[d->count] = r;      /* 같은 포인터를 두 인덱스에 함께 등록 */
    d->count++;
}

add에서는 Rec *r 하나를 만든 다음, 그 포인터를 by_id와 by_name에 둘 다 저장
→이것만으론 owner를 못 정함
판단근거: directory_free의 첫 번째 루프

free(d->by_id[i]->name); 
free(d->by_id[i]);
  • name 문자열까지 free하는 쪽이 by_id
  • by_name은 같은 Rec를 이름순으로 찾아보기 위한 배열일 뿐이므로, 여기서 다시 free()하면 안 된다고 생각함

그래서 by_id가 owner, by_name은 free하면 안 됨. 루프 2는 이 사실을 까먹고 추가된 코드로 보임

5) 수정

by_name을 free()하는 두 번째 루프를 삭제

static void directory_free(Directory *d) {
    for (int i = 0; i < d->count; i++) {
        free(d->by_id[i]->name);
        free(d->by_id[i]);
    }
    /* --- */
    d->count = 0;
}

여러 포인터가 같은 객체를 가리키고 있다면, 그 객체를 free()하는 것은 한 번만 해야 함!

0개의 댓글