디버깅 랩 1번 문제 해결하다가 발견한 것들.
구조체의 render과 on_event는 함수다.
Vtable->on_event이런 식으로 함수를 호출할 수 있다.
screen_dispatch함수에서는 on_event함수를, screen_render에서는 render함수를 호출하고 있다.
함수의 주소가 저장된 Widget에 저장된 VTable의 종류에 따라 호출하는 함수가 달라진다.
VTable은 이렇게 정의되어 있고, widget_noop_event는 아무 일도 하지 않는 더미 함수다.
이 예제로 설명하면,
LABET_VT를 가짐.BUTTON_VT를 가짐.DIALOG_VT를 가짐.각각의 위젯은 가지고 있는 VT 종류에 따라 screen_dispatch와 screen_render함수가 호출될 때 같은 코드에서 각각 다른 함수를 호출한다.
이를 다형성이라고 한다.
각 위젯은 Screen구조체에 포인터 배열로 저장된다.
예제에서 main함수 내부에서 Screen을 다음과 같이 초기화하고 있다.
int main(){
Screen s = { .count = 0 };
.
.
.
}
뭔가 정말 C답지 않은 코드인데...
C99부터 지원되는 Designated Initializer(지정 초기자)문법이라고 한다.
구조체 멤버를 초기화할 때 사용할 수 있다.
위 코드처럼 초기화하면
s.items[0] = NULL
s.items[1] = NULL
s.items[2] = NULL
...
s.items[7] = NULL
s.count = 0
같은 모양이 된다.
초기화하지 않은 다른 멤버 또한 0에 해당하는 값으로 초기화하는 aggregate initialization규칙이 있어서 배열의 멤버들도 NULL로 초기화되는 모양이다.
160번째 줄 screen_dispatch(&s, 1)을 실행한 후 12번 위젯이 free되는데, 해당 위젯을 가르키는 포인터를 그대로 두면서 댕글링 포인터가 됨. app_build_status함수로 인위적인 UAF상황을 만들었고, 168번째 줄에서 screens_render을 다시 호출하고 12번 위젯에 접근하면서 SEGFAULT발생.
Free의 발생은 DIALOG_VT 와 screen_dispatch의 두 번째 매개변수가 1일 때.screen_dispatch함수는 위젯 배열을 순회하면서 on_event함수를 각각 호출함. (기능 다 다름)screen_dispatch → dialog_on_event → widget_destroy → free 순서static void screen_dispatch(Screen *s, int code) {
for (int i = 0; i < s->count; i++) {
Widget *w = s->items[i];
if(((w->vtbl) == &DIALOG_VT) && (code == 1)) s->items[i] = NULL; // here
w->vtbl->on_event(w, code);
}
}
위젯 포인터 w의 vtbl이 DIALOG_VT와 같고 (& 위치 중요함) code가 1일 때 (1이어야 free함) 위젯을 가르키는 원본 포인터를 NULL로 만든다.
(w->vtbl) == &DIALOG_VT
이렇게 비교하는 이유는 위젯을 생성할 때 함수 포인터 구조체의 주소를 전달받았고, Widget의 vtbl도 포인터 변수여서 서로 주소를 비교해야 된다고 생각했음.
그럼 이제 첫 번 째 dispatch함수 호출에서 정상적으로 Free & NULL되었으니, 다음 Render순회에서 NULL인 위젯 슬롯을 건너뛰게 하면 된다,'
static void screen_render(Screen *s) {
for (int i = 0; i < s->count; i++) {
Widget *w = s->items[i];
if(w == NULL) continue; // here
w->vtbl->render(w);
}
}