일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Best of the Best
- DLL 사이드로딩
- 디포전
- 세마포어
- dll side-loading
- 정보기
- malware
- h4ckinggame
- race condition
- 정보보안기사
- 필기
- 디지털 포렌식 트랙
- BoB 12기 최종합격 후기
- 프로그래머스
- CodeEngn
- 뮤텍스
- 디포전 2급
- bob
- cve-2022-26923
- Active Directory
- 코드엔진
- 논문리뷰
- BoB 12기
- 리버싱
- cve-2024-6387
- 디지털 포렌식 전문가 2급
- Today
- Total
SEO
[OS] Race Condition과 세마포어/뮤텍스 본문
최근 CS 지식과 기초 보안 개념을 다시 점검할 기회가 있었습니다.
정말 부끄럽게도, 원리나 세부적인 내용을 깊이 있게 이해하지 못한 채 단순히 정의만 알고 있는 경우가 많았습니다.
그동안 공부한 시간이 적지 않은데도 기본적인 개념조차 헷갈려하는 제 모습을 보며 반성 많이 했습니다. 하하
아무튼! 앞으로 같은 실수를 반복하지 않기 위해 이제부터라도 관련 개념들을 제대로 정리해보려고 합니다..!
Race Condition이란?
여러 개의 스레드(또는 프로세스)가 공유 자원에 접근할 때 실행 순서에 따라 예상치 못한 결과가 발생하는 현상입니다.
Race Condition 문제 상황
현재 상황 : 은행 계좌에 100원의 잔고가 존재하며, 동시에 스레드 1과 2가 실행됨
정상 동작 : 잔고 20원이 남아야 함
- 스레드 1 : 50원 출금
- 스레드 2 : 30원 출금
1. 스레드 1이 현재 잔고 100원을 읽음
2. 스레드 2가 현재 잔고 100원을 읽음
3. 스레드 1이 (100-50) 50원을 계산하고 저장
4. 스데르 2가 (100-30) 70원을 계산하고 저장
5. 최종 잔고는 마지막 저장된 70원이 되며, 실제 정상 동작으로는 20원이 저장되어야 함
=> Race Condition
Race Condition 원인
1. 공유 자원 접근
여러 스레드가 동시에 동일한 자원(변수, 파일 등)을 수정할 때 발생
2. 연산이 원자적이지 않음
읽기/연산/쓰기가 하나의 단위로 실행되지 않으면 Race Condition 발생 가능
3. 동기화 부족
뮤텍스, 세마포어 등의 적절한 동기화가 없으면 실행 순서가 보장되지 않음
Race Condition 해결방안
1. 상호 배제(Mutual Exclusion) - 뮤텍스(Mutex) 사용
- 한 번에 하나의 스레드만 공유 자원에 접근할 수 있도록 제한하여 한 스레드가 자원을 사용하면 다른 스레드는 대기
- Race Condition 방지가 가능하나, 스레드가 대기해야하기 때문에 성능 저하의 위험이 있고, 교착 상태(Deadlock) 위험이 있음
#include <pthread.h>
#include <stdio.h>
int counter = 0;
pthread_mutex_t mutex; // 뮤텍스 선언
void* increment(void* arg) {
pthread_mutex_lock(&mutex); // 뮤텍스 락
counter++; // 공유 자원 접근 (임계 구역)
pthread_mutex_unlock(&mutex); // 뮤텍스 해제
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL); // 뮤텍스 초기화
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Counter: %d\n", counter); // 항상 2가 출력됨
pthread_mutex_destroy(&mutex); // 뮤텍스 제거
return 0;
}
2. 원자적 연산 사용
- CPU가 한 번에 실행할 수 있는 원자적 연산을 사용하여 Race Condition 방지
- atomic 연산을 사용하면 읽기 → 연산 → 쓰기가 단일 연산으로 실행됨
- 뮤텍스보다 속도가 빠르지만 모든 연산을 원자적으로 만들 수는 없음
#include <stdatomic.h>
#include <stdio.h>
atomic_int counter = 0;
void increment() {
atomic_fetch_add(&counter, 1); // 원자적 연산
}
int main() {
increment();
increment();
printf("Counter: %d\n", counter); // 항상 2 출력
return 0;
}
3. 세마포어(Semaphore) 사용
- N개의 스레드가 동시에 공유 자원에 접근할 수 있도록 제한하는 동기화 기법
- 특정 개수의 스레드만 접근 가능하도록 조절 가능하며, 잘못된 사용 시 데드락 발생 가
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
int counter = 0;
sem_t sem; // 세마포어 선언
void* increment(void* arg) {
sem_wait(&sem); // 세마포어 감소 (임계 구역 진입)
counter++;
sem_post(&sem); // 세마포어 증가 (임계 구역 해제)
return NULL;
}
int main() {
pthread_t t1, t2;
sem_init(&sem, 0, 1); // 초기값 1 → 뮤텍스처럼 동작
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Counter: %d\n", counter); // 항상 2 출력
sem_destroy(&sem); // 세마포어 제거
return 0;
}
4. 모니터(Monitor) 사용
- 객체 내부에서 자동으로 동기화를 처리하는 방법
- Java, Python과 같은 언어에서 synchronized 키워드 사용 가능
- 직접 락을 관리할 필요 없으며, 스레드가 대기해야하기 때문에 성능 저하의 위험이 있음
class SharedResource {
private int counter = 0;
public synchronized void increment() { // 락 자동 처리
counter++;
}
public synchronized int getCounter() {
return counter;
}
}
5. 락 프리(Lock-Free) 알고리즘 사용
- 락을 사용하지 않고 Race Condition을 방지하는 방법으로 대표적으로 CAS(Compare And Swap) 연산이 있음
- 락 없이 빠른 동기화가 가능하나, 구현이 복잡하고 일부 연산만 지원함
#include <stdio.h>
int counter = 0;
void increment() {
int expected;
do {
expected = counter;
} while (!__sync_bool_compare_and_swap(&counter, expected, expected + 1));
}
int main() {
increment();
increment();
printf("Counter: %d\n", counter); // 항상 2 출력
return 0;
}
이 부분에 대해서는 아래 블로그에 잘 설명되어있습니다.
Lock Free 알고리즘(Non-Blocking 알고리즘)
병렬 알고리즘과 관련해서 최근의 연구 결과를 보면 대부분이 Non-Blocking 알고리즘, 즉 여러 스레드가 동작하는 환경에서 데이터의 안정성을 보장하는 방법으로 락을 사용하는 대신 저수준의 하
effectivesquid.tistory.com
세마포어(Semaphore)란?
세마포어는 공유 자원에 접근할 수 있는 스레드의 개수를 제한하는 동기화 도구입니다. 특정 자원에 여러 개의 스레드가 접근할 수 있지만, 허용되는 최대 개수를 초과하면 대기해야 합니다.
세마포어는 신호 메커니즘을 기반으로 작동하며, 뮤텍스에 비해 덜 제한적인 제어 메커니즘을 제공합니다. 모든 스레드는 다른 모든 스레드를 호출할 수 있습니다.
세마포어의 동작 방식
1. 특정 자원에 대해 최대 허용 가능한 스레드 개수를 설정합니다.
2. 스레드가 자원에 접근할 때마다 세마포어 값을 감소시킵니다.
3. 자원을 사용한 후에는 세마포어 값을 증가시켜 다른 스레드가 사용할 수 있도록 합니다.
세마포어 종류
1. 카운팅 세마포어 (Counting Semaphore)
- 여러 개의 스레드(프로세스)가 접근할 수 있도록 허용하는 세마포어
- 내부적으로 카운터 값을 가지며, 특정 개수만큼의 동시 접근을 허용함
- sem_wait()을 호출하면 카운터가 감소하고, sem_post()를 호출하면 증가함
sem_t semaphore;
sem_init(&semaphore, 0, 3); // 최대 3개의 스레드 동시 접근 가능
2. 이진 세마포어 (Binary Semaphore)
- 카운터 값이 0 또는 1만 가질 수 있는 세마포어
- 동작 방식은 뮤텍스와 유사하지만, 세마포어는 프로세스 간 동기화에도 사용 가능함
- sem_wait()을 호출하면 0이 되어 다른 스레드는 접근할 수 없고, sem_post()로 다시 1이 되면 접근 가능함
sem_t binary_semaphore;
sem_init(&binary_semaphore, 0, 1); // 1개의 스레드만 접근 가능
이진 세마포어는 뮤텍스와 유사하게 동작하기 때문에 충분히 race condition을 예방할 수 있다고 이해를 하였습니다. 하지만 카운팅 세마포어의 경우, 여러개의 스레드(또는 프로세스)가 동시에 공유 자원에 접근하기 때문에 이 과정에서 경쟁 상태가 발생할 수 있지 않을까? 라는 의문이 생겼습니다.
다행히도 레딧에 저와 같은 궁금증(?)을 가지신 분이 질문을 하여 관련 답글을 참고할 수 있었습니다.
From the golang community on Reddit
Explore this post and more from the golang community
www.reddit.com
질문을 요약하자면 다음과 같은 내용이었습니다.
세마포어를 사용하여 특정 개수(예를 들어, 5개)의 스레드만 공유 메모리에 접근하도록 제한하면, 이 5개의 스레드가 동시에 공유 메모리를 수정할 경우 Race Condition이 발생할 수 있는가?
답변으로는 Race Condition이 발생할 수 있다고 합니다. 세마포어의 주 목적은 몇 개의 스레드가 접근할 수 있는지 제한할 뿐, 스레드들이 공유 자원을 안전하게 사용하는지 보장하지 않는다고 합니다. 하지만 이진 세마포어는 한번에 하나의 스레드만 공유 자원에 접근 가능하기 때문에 뮤텍스처럼 동작할 수 있고 Race Condition을 방지하는데 사용할 수 있습니다.
반면에 카운팅 세마포어는 여러 개의 스레드가 동시에 공유 자원에 접근이 가능하고 두 개의 스레드가 같은 값을 읽고 동시에 증가시키면 한번의 증가가 사라지는 등 Race Condition이 발생할 수 있습니다. 따라서 이런 경우에는 추가적인 뮤텍스를 사용하거나 읽기는 동시 허용, 쓰기는 단독 실행하도록 코드를 추가해야합니다.
그렇다면 이진 세마포어와 뮤텍스의 차이점은 무엇일까요.
구분 | 이진 세마포어 | 뮤텍스 |
값의 범위 | 0 또는 1 | 잠금(Locked) / 해제(Unlocked) |
소유권 (Ownership) | 특정 스레드가 소유하지 않음 | 락을 획득한 스레드만 해제 가능 |
사용 목적 | 단순한 상호 배제 (Mutex 역할 가능) | 상호 배제 및 스레드 간 동기화 |
사용 방식 | sem_wait() / sem_post() | pthread_mutex_lock() / pthread_mutex_unlock() |
즉, 뮤텍스는 락을 건 스레드만이 해제할 수 있고, 이진 세마포어는 아무 스레드나 해체 가능합니다. 따라서 단순한 상호 배제가 필요할 경우 뮤텍스를 사용하고, 프로세스가 동기화를 위해서라면 이진 세마포어를 사용하는 경향이 있다고 합니다.
리눅스의 세마포어
1. System V 기반
semget : 세마포어 생성
semctl : 세마포어 제어
semop : 세마포어 연산
2. POSIX 기반
POSIX 기반 Semaphore는 이름을 가지는 named semaphore와 익명으로 수행되는 unnamed semaphore로 구분됩니다.
- named semaphore
sem_open : 명명된 세마포어를 반환하고 선택적으로 생성
sem_wait : 세마포어의 값을 감소시키며 값이 현재 0이면 차단
sem_post : 세마포어의 값을 증가시키며 값이 0이면 다른 프로세스를 재개
sem_close : 세마포어를 닫음
sem_unlink : 명명된 세마포어 삭제
명명된 세마포어는 name, oflag, mode의 표준 POSIX 인수와 초기 부호 없는 정수 값을 사용하여 생성됩니다. 새로운
세마포어를 생성할 때는 mode 및 value 매개변수를 모두 포함해야하며, 기존 세마포어에 연결할 때는 둘 다 제외해야
합니다. sem_wait() 및 sem_post() 함수는 세마포어의 값을 감소(대기)하거나 증가(게시)시킵니다. 즉, sem_wait()은
sem_post()를 호출하는 다른 프로세스가 값을 변경할 때까지 현재 프로세스를 차단하며, sem_post()는 한번에 하나의
프로세스만 차단을 해제합니다.
name semaphore는 관련 없는 프로세스에서도 사용될 수 있다는 점이 중요합니다.
/* Code Listing 3.11:
Creating and using a POSIX semaphore to control the timing of parent/child execution
*/
/* Create and open the semaphore */
sem_t *sem = sem_open ("/OpenCSF_Sema", O_CREAT | O_EXCL, S_IRUSR | S_IWUSR, 0);
assert (sem != SEM_FAILED);
/* Fork to create the child process */
pid_t child_pid = fork();
assert (child_pid != -1);
/* Note the child inherits a copy of the semaphore connection */
/* Child process: wait for semaphore, print "second", then exit */
if (child_pid == 0)
{
sem_wait (sem);
printf ("second\n");
sem_close (sem);
return 0;
}
/* Parent prints then posts to the semaphore and waits on child */
printf ("first\n");
sem_post (sem);
wait (NULL);
/* Now the child has printed and exited */
printf ("third\n");
sem_close (sem);
sem_unlink ("/OpenCSF_Sema");
- unnamed semaphore
sem_init : 명명되지 않은 세마포어를 생성하고 초기화함
sem_destroy : 명명되지 않은 세마포어를 삭제함
명명되지 않은 세마포어는 공유 메모리에 존재해야 합니다. 즉, 공유 메모리를 사용하는 스레드 간 동기화에는 적합하
지만 프로세스 간(IPC)으로 사용하기 위해서는 shmget, mmap 등을 사용하여 공유 메모리 설정이 필요합니다.
또한 macOS에서는 sem_init()으로 만든 unnamed semaphore가 제대로 동작하지 않는다고 합니다. 컴파일은 되지만
sem_wait()이 즉시 반환되어 동기화가 깨지기 때문에 System V IPC 또는 POSIX named Semaphore를 사용해야 합
니다.
- named semaphore vs unnamed semaphore
구분 | Named Semaphore | Unnamed Semaphore |
저장 위치 | 파일 시스템 (일반적으로 /dev/shm 같은 공유 메모리 영역에 파일 형태로 저장됨) | 프로세스의 메모리 공간 (heap, stack, 공유 메모리 등) |
지속성 (Persistence) | 프로세스가 종료되어도파일이 남아있으면 유지됨 | 프로세스가 종료되면 세마포어도 사라짐 |
공유 방식 | 파일명을 기반으로 여러 프로세스에서 접근 가능 (sem_open("/mysem", ...)) | 같은 프로세스 내의 스레드끼리 공유 (다른 프로세스에서 공유하려면 공유 메모리 필요) |
삭제 방법 | sem_unlink("/mysem")을 호출해야 완전히 삭제됨 | 프로세스 종료 시 자동으로 소멸 |
System V 와 POSIX 기반 Semaphore의 차이는 아래 stackoverflow 질문에 훨씬 더 자세히 설명되어있습니다.
가장 흥미로웠던 차이점은 POSIX는 프로세스가 종료되면 커널이 자동으로 반환하는 방식으로 세마포어 리소스를 반환하지만, System V는 프로세스 종료 시에 자동으로 세마포어가 해제되지 않는다고 합니다.
https://stackoverflow.com/questions/368322/differences-between-system-v-and-posix-semaphores
뮤텍스(Mutex)란?
뮤텍스는 한 번에 하나의 스레드만 공유 자원에 접근할 수 있도록 보장하는 동기화 도구입니다.
뮤텍스의 동작 방식
1. 스레드가 공유 자원에 접근하려면 뮤텍스를 잠금합니다.
2. 다른 스레드는 잠금이 해제될 때까지 대기해야 합니다.
3. 자원 사용이 끝나면 뮤텍스를 해제하여 다른 스레드가 접근할 수 있도록 합니다.
세마포어 vs 뮤텍스
비교 항목 | 세마포어 | 뮤텍스 |
동작 방식 | 카운트를 기반으로 여러 개의 스레드가 접근 가능 | 한 번에 하나의 스레드만 접근 가능 |
사용 목적 | 제한된 개수의 스레드가 공유 자원에 접근하도록 제한 | 단 하나의 스레드만 공유 자원 접근 허용 |
해제 가능 여부 | 다른 스레드가 해제할 수도 있음 | 잠근 스레드만 해제 가능 |
동시 접속 | 여러 개의 스레드/프로세스가 접근 가능 (카운팅 세마포어) | 한 번에 하나의 스레드만 접근 가능 |
락 개념 | 자원의 개수를 조절하는 신호(flag) 역할 | 특정 스레드가 락을 획득해야 함 |
사용 목적 | 리소스 개수 제한 (DB 커넥션 풀 등) | 공유 데이터 보호 (임계 구역 보호) |
프로세스 간 공유 | 가능 (네임드 세마포어) | 일반적으로 불가능 |
재진입 가능 여부 | 가능 (스레드 간 신호 전달 가능) | 기본 뮤텍스는 불가능 (재귀적 뮤텍스 필요) |
속도 | 상대적으로 느림 | 상대적으로 빠름 (스핀락) |
주요 사용 사례 | 데이터베이스 연결 풀, 네트워크 요청 제한 | 임계 영역 보호, 파일 쓰기 |
세마포어는 특정 개수의 동시 접근을 허용, 뮤텍스는 오직 하나의 접근만 허용하는 것이 가장 큰 차이점입니다.
이러한 차이점에서 알 수 있는 점이 뮤텍스는 IPC로 사용되지 않지만 세마포어는 IPC로 사용될 수 있습니다.
세마포어의 경우, 여러 개의 스레드(프로세스)가 공유 자원에 접근할 수 있도록 제어하는 동기화 기법으로 IPC로 사용할 수 있습니다.
Race Condition 관련 1-DAY 취약점
The Chase for Time: Race Condition Vulnerabilities and How to Exploit Them — A Live Example from…
Introduction
medium.com
위 게시글을 2024년에 발견된 OpenSSH의 CVE-2024-6387 취약점을 설명하는 글입니다. CVE-2024-6387은 Race Condition 취약점의 실제 사례로, OpenSSH 서버의 SIGALRM 신호 핸들러에서 발생하는 Race Condition을 이용해 힙 메모리를 조작하고 최종적으로 RCE를 일으킬 수 있습니다.
참고자료
https://www.geeksforgeeks.org/mutex-vs-semaphore/
https://ch4njun.tistory.com/121
https://w3.cs.jmu.edu/kirkpams/OpenCSF/Books/csf/html/IPCSems.html
'Security > System' 카테고리의 다른 글
[heap basics] Tcache와 멀티스레딩 환경 속 Heap 메모리 관리 (1) | 2025.01.18 |
---|---|
[heap exploit] ptmalloc2의 unsorted bin과 Top Chunk (0) | 2025.01.12 |
[heap exploit] Use-After-Free (UAF) 취약점 (0) | 2025.01.12 |