Skip to content

libevent 헤더와 라이브러리 버전 차이로 인한 memcached segfault

Namjae Kim edited this page Nov 2, 2023 · 13 revisions

memcached segfault 개요

아래와 같이 빌드한 memcached 사용할 경우, segmentation fault 발생

  • libevent 2.1.12 헤더 파일을 이용하여 컴파일하고 memcached.o 파일 생성
  • libevent 1.4.13 라이브러리와 링크하여 memcached 바이너리 파일 생성

libevent 버전에 따른 구조체 차이

libevent-1.4.13-stable

memcached 바이너리에 링크되는 libevent 라이브러리 파일(libevent-1.4.13.so)은 아래와 같은 event 구조체를 바탕으로 구현되어 있습니다.

struct event {
	TAILQ_ENTRY (event) ev_next;
	TAILQ_ENTRY (event) ev_active_next;
	TAILQ_ENTRY (event) ev_signal_next;
	unsigned int min_heap_idx;	/* for managing timeouts */

	struct event_base *ev_base;

	int ev_fd;
	short ev_events;
	short ev_ncalls;
	short *ev_pncalls;	/* Allows deletes in callback */

	struct timeval ev_timeout;

	int ev_pri;		/* smaller numbers are higher priority */

	void (*ev_callback)(int, short, void *arg);
	void *ev_arg;

	int ev_res;		/* result passed to event callback */
	int ev_flags;
};

libevent-2.1.12-stable

memcached 바이너리의 컴파일 시 사용된 libevent 헤더 파일(event.h)에는 아래와 같은 event 구조체가 정의되어 있습니다.

struct event {
	struct event_callback ev_evcallback;

	/* for managing timeouts */
	union {
		TAILQ_ENTRY(event) ev_next_with_common_timeout;
		int min_heap_idx;
	} ev_timeout_pos;
	evutil_socket_t ev_fd;

	struct event_base *ev_base;

	union {
		/* used for io events */
		struct {
			LIST_ENTRY (event) ev_io_next;
			struct timeval ev_timeout;
		} ev_io;

		/* used by signal events */
		struct {
			LIST_ENTRY (event) ev_signal_next;
			short ev_ncalls;
			/* Allows deletes in callback */
			short *ev_pncalls;
		} ev_signal;
	} ev_;

	short ev_events;
	short ev_res;		/* result passed to event callback */
	struct timeval ev_timeout;
};

Process 구동부터 segmentation fault 발생까지

  1. event 구조체 초기화

    // struct event_base *main_base = event_base_new();
    /* ... */
    
    /* new connection */
    event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
    event_base_set(main_base, &c->event);

    캐시 서버 구동 후 새로운 connection이 생성되면, memcached는 위와 같이 libevent의 함수를 호출하여 event 구조체를 초기화하게 됩니다.

    이때, 함수의 구현은 링크된 shared object(v1.4.13) 내부의 구현을 사용하게 되므로 c->event는 1.4.13 version의 구조에 맞는 값을 가지게 되고, memcached가 이해하고 있는 event 구조체(v2.1.12)와 mismatch 발생합니다.

  2. update_event() 함수 로직

    struct event_base *base = c->event.ev_base;  // invalid value
    event_set(&c->event, c->sfd, new_flags, event_handler, (void *)c);
    event_base_set(base, &c->event);  /* SEGMENTATION FAULT */

    위와 같이 event 구조체가 잘못 초기화된 상황에서 일정 개수(-R option에 의해 결정, default=20)의 요청을 처리하는 시점에 memcached는 event를 새로 설정하는 과정에서 event_base_set()의 event_base 인자 값으로 c->event.ev_base 값을 넘기게 됩니다.

    memcached는 2.1.12 version의 헤더를 바탕으로 event 구조체를 해석하므로, c->event.ev_base 위치에는 event_base 주소가 아닌 다른 값이 위치한 상태이고, 이를 event_base 주소로 넘기면서 segfault 문제가 발생하게 됩니다.

libevent 헤더와 라이브러리에 차이가 발생한 이유

설치 과정 중 ./configure 수행 시 인자에 따라서 컴파일에 사용할 헤더와 라이브러리를 탐색하게 됩니다. 이 때 탐색 우선순위는 다음과 같습니다.

  1. --with-libevent 또는 --with-zookeeper에 의해 지정된 경로
  2. (system path): gcc 기본 참조 경로
  3. --prefix에 의해 지정된 경로

이를 바탕으로 문제가 발생하는 시나리오는 다음과 같습니다.

  1. 사전 조건

    • libeventzookeeper가 동일 경로에 설치된 상태
      <target_path>
      ├── include
      │   ├── event.h (libevent 1.14.3)
      │   └── zookeeper
      │       └── zookeeper.h
      └── lib
          ├── libevent-1.14.3.so
          └── libzookeeper_mt-3.5.9-p3.so.2
      
    • libevent가 system path에 이미 설치된 상태
      <system_path>
      ├── include
      │   └── event.h (libevent 2.1.12)
      └── lib
          └── libevent-2.1.12.so
      
  2. ./configure --enable-zk-integration --prefix=<target_path> 명령 수행

    1. libevent 설치 위치 탐색
      1. --with-libevent option 설정하지 않았으므로 pass
      2. gcc 기본 탐색 경로에서 libevent 발견
      3. gcc 컴파일 옵션 추가하지 않음
    2. zookeeper 설치 위치 탐색
      1. --with-zookeeper option 설정하지 않았으므로 pass
      2. gcc 기본 탐색 경로에서 zookeeper 발견 실패
      3. --prefix 위치에서 zookeeper 발견
      4. gcc 컴파일 옵션 추가 gcc -I <target_path>/include/zookeeper -L <target_path>/lib
  3. make 수행
    gcc는 -I option 또는 -L option이 지정된 경우 해당 경로를 우선 탐색하고, 발견하지 못한 경우에 한해 기본 경로 탐색

    • 헤더
      1. -I 위치에서 zookeeper 헤더 발견 / libevent 헤더 발견 실패
      2. system path에서 libevent 2.1.12 버전의 헤더 발견
    • 라이브러리
      1. -L 위치에서 zookeeper.so 발견 / libevent.1.4.13.so 발견
  4. 결과적으로 libevent의 헤더는 ./configure 수행 결과와 동일하게 system path를 사용하지만,
    라이브러리는 zookeeper에 의해 추가된 -L option의 영향을 받아 prefix 경로의 라이브러리를 사용하게 됩니다.

위와 같은 문제 발생하지 않게 하려면

  1. ./configure 수행 시 --with-libevent=<path> option 사용
    • --with-libevent 설정하는 경우 system path보다 지정된 경로를 먼저 탐색하고 -I-L 옵션을 추가하기 때문에 문제 발생하지 않게 됩니다.
  2. 다음 릴리즈 버전(>1.13.4) 사용
    • 이번 문제 확인 이후로 라이브러리 탐색 순서를 변경하였습니다.
      • 기존: --with-libevent=<path> => (system path) => --prefix=<path>
      • 변경: --with-libevent=<path> => --prefix=<path> => (system path)
    • 따라서, prefix에 libevent가 설치되어 있는 경우 system path를 탐색하지 않습니다.