문제풀이를 위해 공부한 개념
Use After Free
말 그대로 free() 한 메모리를 다시 사용하는 것이다.
free(p) 가 하는 일 : 이 영역 이제 안 쓴다고 할당자(allocator) 에게 반납한다.
free(p) 가 하지 않는 일 : 메모리 내용을 지우지 않는다. p 값 자체도 건드리지 않는다.
free 직후의 p 는 여전히 그 주소를 가리키고 있다. 이런 포인터를 dangling pointer(허상 포인터) 라고 부른다.
strncpy
문자열을 복사하는 함수
#include <string.h>
char *strncpy(char *dest, const char *src, size_t n);
- src 에서 최대 n 바이트를 dest 에 복사
- 반환값 : dest 의 주소
- 동작
- strlen(src) < n → 복사 후 남은 칸을 전부 ‘\0’ 으로 채움
- strlen(src) ≥ n → n 바이트만 복사하고 ‘\0’을 붙이지 않음 (흔한 버그 원인)
src 가 가리키는 문자열을 dest 가 가리키는 공간에 복사하고, dest 를 그대로 반환한다. 이름 그대로 stringcopy 이다. 몇 바이트까지 복사할 지 알려줘야 한다. NULL 종료를 보장하지 않아서 위험할 때가 많다. 버퍼 크기가 고정이고 소스 길이를 모를 때 쓰는 함수이다.
strcpy
문자열을 복사하는 함수
#include <string.h>
char *strcpy(char *dest, const char *src);
strcpy 와 strncpy 차이
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n); // n = 최대 복사 바이트 수
GDB 디버깅 기본 흐름
run → bt → frame N → info locals
(gdb) run → 크래시(SIGSEGV)
(gdb) bt → screen_render() 안 w->vtbl->render(w) 지점
(gdb) print w → 어떤 위젯인지(주소/슬롯) 확인
(gdb) print w->vtbl → 오염돼 있음
(gdb) print s->items[2] → 이미 해제된 슬롯이 그대로 남아있음
(gdb) break widget_destroy → 누가/언제 이 위젯을 free 하는지 역추적
NULL 역참조
값이 NULL인 포인터를 통해 메모리에 접근하려는 것. *p 나 p→member 처럼 포인터가 가리키는 곳을 읽거나 쓰는 동작을 역참조라고 한다. 그 포인터가 아무것도 가리키지 않는 NULL이면 문제가 생긴다.
malloc & free 함수
malloc 한번에 free 한번, 반납 대상은 malloc 이 돌려준 그 주소 그대로여야한다. malloc 은 함수 안에서는 할당한 쪽과 해제하는 쪽이 달라야 한다. 함수 밖에서 사용하려면 리턴을 말록에 할당한 변수로 할당하고, 사용해야한다. 함수가 반환한 포인터는 호출한 쪽이 free 해야 한다.
const
함수 안에서 변수가 가리키는 내용을 바꾸지 않겠다는 뜻이다. 포인터 변수 자체는 바꿀 수 있다.
void f(const char *p){
p[0] = 'X'; // error
p = "other"; // OK
}
memset
#include <string.h>
void *memset(void *ptr, int value, size_t n);
- ptr 부터 n 바이트를 value 로 채움
- 반환값 : ptr 의 주소
문제
https://github.com/yeooonee/Jungle-C-DebuggingLab/blob/main/challenges/01_use_after_free/bug.c
Jungle-C-DebuggingLab/challenges/01_use_after_free/bug.c at main · yeooonee/Jungle-C-DebuggingLab
Krafton jungle week 5 debugging lab. Contribute to yeooonee/Jungle-C-DebuggingLab development by creating an account on GitHub.
github.com
문제 풀이
전체 흐름
큰 흐름은 먼저 main 에서 widget 을 생성 하고, screen 에 추가한다.
int main(void) {
Screen s = { .count = 0 };
screen_add(&s, widget_new(&LABEL_VT, 10, "Welcome"));
screen_add(&s, widget_new(&BUTTON_VT, 11, "OK"));
screen_add(&s, widget_new(&DIALOG_VT, 12, "Are you sure?")); /* items[2] */
screen_add(&s, widget_new(&BUTTON_VT, 13, "Cancel"));
printf("frame 1:\n");
screen_render(&s);
screen_dispatch(&s, 1);
/* TODO 닫힌(closed) 위젯을 여기서 정리(free + 해당 슬롯 NULL)할 필요가 있음 */
char *status = app_build_status("dialog closed");
printf("%s\n", status);
printf("frame 2:\n");
screen_render(&s);
free(status);
for (int i = 0; i < s.count; i++) free(s.items[i]);
return 0;
}
상세한 흐름은 아래와 같다.
// screen_add 호출
Screen s = { .count = 0 };
screen_add(&s, widget_new(&DIALOG_VT, 12, "Are you sure?")); /* items[2] */
↓
// DIALOG_VT 확인
static const VTable DIALOG_VT = { dialog_render, dialog_on_event };
↓
// dialog_render, dialog_on_event 함수 확인
static void dialog_render(Widget *self) {
printf(" <<Dialog #%d>> %s\n", self->id, self->label);
}
static void dialog_on_event(Widget *self, int code) {
if (code == 1) {
self->closed = 1;
}
}
↓
// widget_new 함수 확인 (새로운 위젯생성)
static Widget *widget_new(const VTable *vt, int id, const char *label) {
// w 에 아직 아무 값도 넣지 않았는데, sizeof *w 로 *w 를 써도 괜찮은 이유는?
// => malloc(sizeof *w) 는 변수에 접근하지 않고 변수의 타입만 가져옴
Widget *w = malloc(sizeof *w);
if (!w) { perror("malloc"); exit(1); }
w->vtbl = vt;
w->id = id;
w->closed = 0;
strncpy(w->label, label, sizeof(w->label) - 1);
w->label[sizeof(w->label) - 1] = '\0';
return w;
}
↓
// Screen s 에 추가됨 (s->item 의 Widget *items[MAX_WIDGETS];이 배열 형태로 사용 가능)
// s 의 item 에 LABEL_VT -> BUTTON_VT -> DIALOG_VT -> BUTTON_VT 순으로 있음.
// screen_dispatch 에서 1 값이 넘어감. vtbl의 event 실행됨.
// dialog_on_event 함수에서 code == 1 로 closed 됨
static void dialog_on_event(Widget *self, int code) {
if (code == 1) {
self->closed = 1;
}
}
↓
// 위젯 닫혔는지 확인 후 free & 연결된 포인터 null 처리
for (int i = 0; i < s.count; i++){
if (s.items[i] == NULL) continue;
if (s.items[i]->closed == 1){
widget_destroy(s.items[i]);
s.items[i] = NULL;
// s.count--; // count 를 -- 하고 빈공간에 뒷 인덱스를 다 끌어당기는 방법도 있는데, 인덱스 관리가 복잡해서 null 로 두는 방법 선택
}
}
수정한 코드
- dialog_on_event 안에서 free 를 제거
- free & 포인터 null 처리 추가
- s->item 접근 시 null 제외 처리 (null 역참조 에러 예방)
어려웠던 점
- 생소하고 모르는 개념도 많고 (위에 정리함), gdb 디버깅 툴도 어렵고, 어떻게 해야할지는 알겠는데 코드를 어떻게 짜야할지 몰랐다.. 너무 어려웠음.
- 결국 AI 힌트 받아서 해결했다. (17일 저녁 8시 ~ 18일 오후 2시까지 고민하다가 AI 도움 받아서 해결함 ㅜ0ㅜ)