본문으로 이동
메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.
Race condition; 경쟁 상태, 경합 조건, 레이스 컨디션
둘 이상의 실행 흐름이 공유 자원에 접근하는 순서나 타이밍에 따라 결과가 달라지는 상태

경쟁 상태는 여러 스레드, 프로세스, 요청, 신호가 같은 자원을 거의 동시에 다룰 때 누가 먼저 실행되느냐에 따라 결과가 바뀌는 상황이다. 대부분의 실행 순서에서는 멀쩡하다가 특정한 끼어들기가 일어났을 때만 값이 틀리거나 보안 검사가 무력화되므로, 재현이 어렵고 시험에서 잘 드러나지 않는다. 멀티코어 프로그래밍, 운영체제 커널, 데이터베이스, 웹 서비스, 디지털 회로에서 모두 나타나며, 공격자가 타이밍을 조작해 권한을 얻거나 한도를 우회하는 취약점이 되기도 한다(CWE-362).[1]

해결의 핵심은 공유 자원을 다루는 구간을 임계 구역으로 정하고 한 번에 하나만 들어가게 하거나(상호 배제), 확인과 변경을 쪼갤 수 없는 하나의 동작(원자 연산)으로 묶는 것이다. 논리 회로에서 신호 경로의 지연 차이로 생기는 경쟁 상태는 1950년대 스위칭 회로 연구에서 이미 쓰이던 용어로 알려져 있다.

정의와 기본 구조

편집 원본 편집

경쟁 상태가 생기려면 다음 조건이 모두 필요하다.

  • 둘 이상의 실행 흐름이 동시에(또는 번갈아) 진행된다. 멀티코어의 진짜 병렬 실행이든, 단일 코어에서 문맥 교환이나 인터럽트로 번갈아 실행되든 마찬가지다.
  • 둘이 같은 자원(메모리 변수, 파일, DB 행, 장치, 계좌 잔액)을 공유한다.
  • 적어도 하나가 그 자원을 바꾼다.
  • 접근 순서를 강제하는 동기화가 없거나 부족하다.

코드에서 자주 보이는 형태는 둘이다.

형태 예 문제
읽고 고쳐서 쓰기(read-modify-write) count++, balance = balance - amount 두 흐름이 같은 옛 값을 읽고 각자 계산해 쓰면 한쪽 변경이 사라진다(갱신 손실)
확인한 뒤 실행(check-then-act) "파일이 없으면 만든다", "재고가 있으면 판다", "쿠폰을 안 썼으면 적용한다" 확인과 실행 사이에 다른 흐름이 상태를 바꾸면 확인 결과가 이미 틀린 채로 실행된다. 보안에서는 TOCTOU라고 부른다

카운터 증가 예

편집 원본 편집

count++는 한 줄이지만 CPU에서는 보통 메모리에서 레지스터로 읽기, 1 더하기, 메모리에 쓰기의 세 명령으로 실행되고, 어느 명령 사이에서든 다른 스레드로 전환될 수 있다.

순서 스레드 A 스레드 B count(메모리)
1 count를 읽음(0) 0
2 1을 더함(레지스터 1) 0
3 count를 읽음(0) 0
4 1을 더함(레지스터 1) 0
5 1을 씀 1
6 1을 씀 1

두 번 증가했는데 결과는 1이다. 다음 Java 코드는 스레드 두 개가 10만 번씩 증가시키지만, 실행할 때마다 20만보다 작은 제각각의 값이 나온다.

class Counter {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Runnable work = () -> { for (int i = 0; i < 100_000; i++) count++; };
        Thread t1 = new Thread(work), t2 = new Thread(work);
        t1.start(); t2.start();
        t1.join();  t2.join();
        System.out.println(count);   // 200000이 아니라 예를 들어 108512, 131277 ...
    }
}

고치는 방법은 세 가지다. synchronized 블록(또는 ReentrantLock)으로 증가 구간을 상호 배제하거나, AtomicInteger.incrementAndGet() 같은 원자 연산을 쓰거나, 스레드마다 따로 세고 마지막에 합치는(공유하지 않는) 것이다. 잠금은 구현이 쉽지만 대기와 문맥 교환 비용이 들고, 원자 연산은 단순한 갱신에서 훨씬 가볍다.

비유: 우유(달걀)가 너무 많은 문제

편집 원본 편집

운영체제 교재에서 흔히 쓰는 예다. 두 룸메이트가 "냉장고에 우유가 없으면 사 온다"는 규칙을 따르면, 둘이 거의 동시에 빈 냉장고를 보고 둘 다 사 오는 일이 생긴다. "사러 간다"는 메모를 붙이기로 해도, 메모가 없는 것을 확인한 순간과 메모를 붙이는 순간 사이에 상대가 끼어들면 똑같은 일이 생긴다. 메모를 붙인 뒤 상대 메모가 있는지 다시 확인하게 하면 이번에는 둘 다 물러나 아무도 사 오지 않을 수 있다. 결국 "확인하고 메모 붙이기"를 누구도 끼어들 수 없는 한 동작으로 만들어야 풀리며, 이것을 추상화한 도구가 뮤텍스와 세마포어다.

데이터 경쟁과의 차이

편집 원본 편집

데이터 경쟁(data race)은 메모리 모델에서 정의하는 좁은 개념이다. 두 스레드가 같은 메모리 위치에 접근하고, 적어도 하나가 쓰기이며, 둘 사이에 동기화(happens-before 관계)가 없으면 데이터 경쟁이다.[2]

경쟁 상태 데이터 경쟁
기준 결과가 타이밍에 따라 달라져 틀릴 수 있는가(논리 문제) 동기화 없는 동시 메모리 접근이 있는가(메모리 모델 위반)
데이터 경쟁 없는 경쟁 상태 모든 접근이 원자적이어도 생긴다. 예: if (atomic_load(&x) == 0) atomic_store(&x, 1);은 확인과 저장이 따로 원자적이라 둘 다 0을 볼 수 있다
경쟁 상태 없는 데이터 경쟁 결과에 영향이 없는 통계 카운터를 동기화 없이 읽는 경우처럼 논리적으로는 무해해 보여도 언어 규칙 위반이다
언어의 취급 논리 버그 C11과 C++11 메모리 모델에서 정의되지 않은 동작(undefined behavior)[3]. Java는 정의되지 않은 동작은 아니지만 예상 밖의 값이 보일 수 있다

데이터 경쟁은 값이 틀리는 것 이상의 문제를 일으킨다. 컴파일러와 CPU는 동기화가 없는 접근을 재배치하거나 레지스터에 캐시할 수 있어 다른 스레드의 변경이 영원히 보이지 않을 수 있고, 한 번에 쓰이지 않는 큰 값(32비트 CPU의 64비트 정수 등)은 두 쓰기의 조각이 섞인 값(찢어진 쓰기, torn write)이 될 수 있다. Rust는 소유권과 Send/Sync 트레이트로 안전한 코드에서의 데이터 경쟁을 컴파일 단계에서 막지만, 논리적인 경쟁 상태까지 막지는 않는다.[4]

방법 원리 예 주의점
뮤텍스(락) 임계 구역에 한 번에 한 스레드만 들어간다 pthread_mutex, C++ std::mutex, Java synchronized, Go sync.Mutex, Python threading.Lock 교착상태, 우선순위 역전, 대기 비용
세마포어 카운터로 동시 진입 수를 N개로 제한하거나 순서를 신호로 맞춘다 POSIX sem_t, Java Semaphore 신호 누락, 잘못된 짝
읽기·쓰기 락 읽기는 여럿, 쓰기는 하나만 pthread_rwlock, Java ReentrantReadWriteLock 쓰기 기아
모니터·조건 변수 락과 "조건이 될 때까지 대기"를 묶는다 Java wait/notify, pthread_cond 조건은 반드시 루프에서 다시 확인(가짜 깨어남)
원자 연산 하드웨어가 읽기·수정·쓰기를 한 명령으로 보장한다 x86 lock xadd, lock cmpxchg, ARM LL/SC, C11 atomic_fetch_add, Java AtomicInteger, Go sync/atomic 단일 변수에만 적용. 여러 변수를 함께 바꾸려면 락 필요
락프리·대기 없는(wait-free) 알고리즘 비교 후 교환(CAS)을 성공할 때까지 반복한다 락프리 큐, ConcurrentHashMap ABA 문제, 메모리 회수, 설계 난이도
불변 객체와 공유하지 않기 바꾸지 않으면 경쟁이 없다. 공유하지 않으면 동기화가 필요 없다 함수형 자료 구조, 스레드 지역 저장소, 행위자 모델과 메시지 전달, Go 채널 복사 비용
트랜잭션 여러 변경을 원자적으로 묶는다 DB 트랜잭션, 소프트웨어 트랜잭셔널 메모리 격리 수준에 따라 이상 현상이 남는다

원자 연산을 쓸 때는 메모리 순서도 함께 정해야 한다. C11/C++11의 memory_order_seq_cst, acquire/release, Java의 volatile은 값 자체뿐 아니라 그 앞뒤 접근의 재배치를 제한한다. 초기화를 한 번만 하려는 이중 확인 잠금(double-checked locking)은 이런 순서 보장 없이 쓰면 반쯤 초기화된 객체가 보이는 경쟁 상태로 유명하다.

/* C11: 원자 연산으로 고친 카운터 */
#include <stdatomic.h>
#include <pthread.h>
atomic_int count = 0;
void *work(void *arg) {
    for (int i = 0; i < 100000; i++)
        atomic_fetch_add(&count, 1);   /* 읽기, 더하기, 쓰기를 한 번에 */
    return NULL;
}

교착 상태와의 관계

편집 원본 편집

경쟁 상태를 막으려고 락을 쓰면 반대편 문제인 교착상태가 생길 수 있다. 스레드 1이 락 A를 잡고 B를 기다리고, 스레드 2가 B를 잡고 A를 기다리면 둘 다 영원히 멈춘다. 상호 배제, 점유 대기, 비선점, 순환 대기의 네 조건이 모두 성립할 때 생기므로, 락을 항상 같은 순서로 잡거나, 시간 제한이 있는 획득(tryLock)을 쓰거나, 락 하나의 범위를 좁혀 대응한다. 락을 너무 잘게 나누면 경쟁 상태가 다시 생기고, 너무 크게 잡으면 병렬성이 떨어지고 교착 위험이 커진다. 서로 양보만 하다 진행이 안 되는 라이브락, 특정 스레드가 계속 밀리는 기아도 같은 계열의 문제다.

하드웨어와 전자 회로

편집 원본 편집

논리 회로에서도 같은 원천에서 나온 신호가 서로 다른 경로를 거쳐 한 게이트에 도착하면 경로마다 전파 지연이 달라 경쟁이 생긴다.

  • 글리치(glitch): 출력이 잠깐 틀린 값으로 튀었다가 제자리로 돌아오는 현상. 이론상 항상 0인 A AND (NOT A) 회로도, A가 0에서 1로 바뀔 때 NOT 게이트를 거친 신호가 늦게 바뀌면 짧은 순간 두 입력이 모두 1이 되어 출력에 펄스가 생긴다.
  • 해저드(hazard): 글리치가 생길 수 있는 회로 구조. 출력이 1로 유지되어야 하는데 순간 0이 되는 정적 1 해저드, 반대인 정적 0 해저드, 한 번 바뀌어야 할 출력이 여러 번 흔들리는 동적 해저드가 있다.
  • 비동기 순차 회로에서는 상태 변수 여러 개가 동시에 바뀌어야 할 때 어느 것이 먼저 바뀌느냐에 따라 최종 상태가 달라지면 치명적 경쟁(critical race), 결과가 같으면 비치명적 경쟁이라고 부른다.
  • 카운터의 비트들이 정확히 같은 순간에 바뀌지 않으면 중간값이 잠깐 나타나 이를 감시하는 비교 회로가 잘못 반응할 수 있다. 그레이 코드는 한 번에 한 비트만 바뀌게 해 이를 피한다.

글리치가 클록이나 래치 입력으로 들어가면 순간적인 오류가 기억 소자에 저장되어 영구적인 오동작이 된다. 대응으로 카르노 맵에서 인접 항을 덮는 중복 항을 추가해 정적 해저드를 없애고, 플립플롭을 쓰는 동기식 설계로 신호가 안정된 뒤 클록 에지에서만 값을 읽는다. 서로 다른 클록 영역을 건너는 신호는 플립플롭을 2단 이상 둔 동기화기로 받아 준안정 상태(metastability)가 퍼지지 않게 한다. 반대로 제조 편차에 따라 어느 경로가 먼저 도착하는지가 칩마다 달라지는 성질을 이용해 물리적 복제 방지 기능(PUF)이나 하드웨어 난수 발생기를 만들기도 한다.

파일 시스템과 TOCTOU

편집 원본 편집

TOCTOU(Time-Of-Check to Time-Of-Use)는 검사 시점과 사용 시점 사이에 대상이 바뀌는 경쟁 상태다(CWE-367).[5] 전형적인 예는 SetUID 프로그램이 access()로 실제 사용자에게 권한이 있는지 확인한 뒤 open()으로 여는 코드다. 두 호출 사이에 공격자가 파일을 /etc/shadow를 가리키는 심볼릭 링크로 바꾸면, 확인은 공격자 파일로 통과하고 열기는 root 권한으로 중요 파일에 적용된다.

/* 취약: 확인과 사용이 분리되어 있다 */
if (access(path, W_OK) == 0) {
    int fd = open(path, O_WRONLY);   /* 이 사이에 path가 바뀔 수 있다 */
    write(fd, buf, len);
}

/* 개선: 한 번의 시스템 호출로 만들고, 연 뒤에는 파일 디스크립터로만 다룬다 */
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
if (fd < 0) { /* 이미 있거나 링크면 실패 */ }
fstat(fd, &st);                     /* 이름이 아니라 연 파일 자체를 검사 */

대응 원칙:

  • 이름(경로)으로 두 번 접근하지 말고, 한 번 연 파일 디스크립터로 검사(fstat)와 사용을 한다. openat처럼 디렉터리 디스크립터 기준 호출을 쓴다.
  • 임시 파일은 이름을 예측할 수 없고 원자적으로 만드는 mkstemp()나 mktemp를 쓴다. tmpnam(), mktemp()(C 함수)로 이름만 받은 뒤 여는 방식은 위험하다.
  • 파일 생성은 O_CREAT | O_EXCL로 "없을 때만 만들기"를 원자적으로 한다. 여러 프로세스가 같은 파일을 고칠 때는 flock, fcntl 잠금이나 lockfile로 조정한다.
  • 권한 있는 프로그램은 작업 전에 실제 사용자 권한으로 낮춘다(seteuid). 리눅스는 누구나 쓸 수 있는 스티키 디렉터리(/tmp)에서 남의 심볼릭 링크를 따라가지 못하게 하는 fs.protected_symlinks 설정을 제공한다.[6]

파일 시스템에서는 서로 관계없는 프로그램이 디스크 공간, 메모리, 파일 디스크립터 같은 공용 자원을 동시에 소진해 서로를 망가뜨리는 넓은 의미의 경쟁도 있다. 작업 전에 필요한 자원을 미리 확보하거나 실패 지점마다 오류 처리를 두는 방식으로 대응한다.

보안 취약점으로서의 경쟁 상태

편집 원본 편집

경쟁 상태는 공격자가 타이밍을 조작할 수 있으면 서비스 거부, 권한 상승, 인증 우회로 이어진다.[1] 정보보안기사 등 시험에서 자주 다루는 유형은 SetUID 프로그램과 임시 파일, 심볼릭 링크를 이용한 공격이다. 버그가 있는 시스템 프로그램과 공격자의 익스플로잇 프로그램이 거의 같은 시간에 실행되면, 공격자 프로그램이 시스템 프로그램의 권한으로 무언가를 하게 된다.

공격이 성립하는 일반적 조건:

  • 대상 파일(프로그램)의 소유자가 root다.
  • SetUID 비트가 설정되어 있다.
  • 프로그램이 실행 중에 만드는 임시 파일의 이름을 공격자가 미리 알 수 있다.

일반적인 절차:

  1. 취약한 프로그램이 만드는 임시 파일의 이름을 알아낸다.
  2. 그 임시 파일과 같은 이름으로, 공격자가 바꾸고 싶은 파일(예: /etc/passwd)을 가리키는 심볼릭 링크를 만든다(ln -s).
  3. 취약한 프로그램이 임시 파일을 지우고 다시 만드는 순간을 노려 링크를 계속 다시 만들며 기다린다.
  4. 프로그램이 링크가 걸린 이름으로 파일을 열어 쓰는 순간, 쓰기는 링크 대상인 중요 파일에 root 권한으로 적용된다.
  5. 시스템은 자신이 만든 임시 파일에 쓴 것으로 알고 계속 진행하고, 공격자는 관리자 권한으로 실행되는 프로그램의 동작에 끼어든 셈이 된다.

그 밖의 대표 유형:

유형 내용
커널 경쟁 커널 코드의 경쟁으로 권한 상승. Dirty COW(CVE-2016-5195)가 대표적이다(아래 사고 사례)
시그널 핸들러 경쟁 비동기 시그널 처리기에서 재진입 불가능한 함수(malloc, syslog 등)를 호출해 메모리가 망가지는 경우
웹 애플리케이션의 한도 초과(limit overrun) 아래 웹 절
인증·세션 비밀번호 재설정 토큰, OTP, CAPTCHA를 동시 요청으로 여러 번 사용

데이터베이스

편집 원본 편집

DB는 트랜잭션으로 동시 접근을 조정하지만 격리 수준과 쿼리 작성 방식에 따라 경쟁 상태가 남는다.

이상 현상 내용 막는 격리 수준(ANSI 기준)
갱신 손실(lost update) 두 트랜잭션이 같은 값을 읽고 각자 고쳐 써서 한쪽 변경이 사라짐 표준 표에는 없음. 제품마다 다르며 PostgreSQL은 REPEATABLE READ에서 충돌을 오류로 알린다
더티 리드 커밋되지 않은 값을 읽음 READ COMMITTED 이상
반복 불가능한 읽기 같은 행을 두 번 읽을 때 값이 다름 REPEATABLE READ 이상
팬텀 같은 조건으로 두 번 조회할 때 행 수가 다름 SERIALIZABLE
쓰기 치우침(write skew) 두 트랜잭션이 서로 다른 행을 읽고 조건을 확인한 뒤 각각 써서, 합치면 규칙이 깨짐(예: 당직 의사가 둘 다 빠짐) SERIALIZABLE(PostgreSQL의 SSI 등)

자세한 내용은 트랜잭션 격리 수준, 데이터베이스 병행제어, Dirty Read (Database) 문서를 참고한다. 격리 수준을 올리지 않고 막는 흔한 방법은 다음과 같다.[7]

-- 1) 확인과 변경을 한 문장으로: 조건부 원자 갱신
UPDATE stock SET qty = qty - 1 WHERE item_id = 42 AND qty > 0;   -- 영향받은 행이 0이면 품절

-- 2) 비관적 잠금: 읽을 때 행을 잠근다
BEGIN;
SELECT balance FROM account WHERE id = 7 FOR UPDATE;
UPDATE account SET balance = balance - 1000 WHERE id = 7;
COMMIT;

-- 3) 낙관적 잠금: 버전 열로 충돌을 감지한다
UPDATE post SET body = '...', version = version + 1 WHERE id = 3 AND version = 5;  -- 0행이면 다시 시도

-- 4) 유일 제약: "이미 썼는지" 검사를 DB가 원자적으로 한다
CREATE UNIQUE INDEX ON coupon_use (coupon_id, user_id);

애플리케이션에서 SELECT로 확인하고 코드에서 판단한 뒤 UPDATE하는 방식은 그 사이에 다른 요청이 끼어들 수 있어 check-then-act 경쟁이 된다. 관련 문서: 데이터베이스 로킹, 낙관적 병행제어, 다중 버전 병행제어.

웹 애플리케이션

편집 원본 편집

웹 서버는 요청을 여러 스레드나 여러 서버에서 동시에 처리하므로, "확인 → 처리 → 기록" 사이의 틈을 노린 동시 요청이 한도를 넘어서는 결과를 낸다. 이런 공격을 한도 초과(limit overrun) 경쟁이라고 한다.

  • 선불 카드, 상품권, 할인 쿠폰, 추천인 보상을 한 번만 쓸 수 있어야 하는데 동시에 여러 번 적용된다.
  • 잔액보다 많은 금액을 출금, 송금, 결제한다. 같은 주문이 두 번 결제된다(중복 결제).
  • 선착순 이벤트, 한정 판매에서 재고보다 많이 팔린다.
  • 로그인 시도 횟수 제한, 인증번호 시도 제한, CAPTCHA 한 번 풀이를 동시 요청으로 우회한다.
  • 상품 평점, 투표, 좋아요를 여러 번 한다.

2023년 PortSwigger의 제임스 케틀(James Kettle)은 HTTP/2 요청 여러 개를 TCP 패킷 하나에 담아 끝을 동시에 보내는 단일 패킷 공격(single-packet attack)을 발표했다. 네트워크 지연의 흔들림을 없애 요청 20~30개를 서버에 거의 같은 순간 도착시킬 수 있어, 원격 경쟁 상태를 로컬만큼 쉽게 재현할 수 있게 되었다.[8][9] 방어 방법:

  • 한도 검사와 차감을 DB의 원자적 조건부 갱신이나 유일 제약으로 처리한다(위 SQL).
  • 결제·송금 API에는 멱등성 키(idempotency key)를 받아 같은 요청의 재전송을 한 번만 처리한다.
  • 여러 서버에 걸친 자원은 DB 행 잠금, Redis 같은 분산 락, 단일 작업 큐로 직렬화한다. 분산 락은 만료 시간과 소유자 확인이 없으면 그 자체가 경쟁 상태가 된다.
  • 세션 단위 잠금에 기대지 않는다. 한 사용자의 동시 요청도 서로 다른 스레드에서 처리될 수 있다.
사례 내용
Therac-25(1985~1987) 캐나다 AECL의 방사선 치료기 Therac-25가 1985년 6월~1987년 1월 사이 6건의 대량 과다 조사 사고를 일으켜 환자가 사망하거나 중상을 입었다. 조사 결과 여러 작업(task)이 동기화 없이 공유 변수를 쓰는 구조였다. 한 사고에서는 조작자가 치료 모드를 입력했다가 벤딩 자석 설정에 걸리는 약 8초 안에 빠르게 수정하면, 설정 작업이 수정을 인식하지 못해 입력과 다른 상태로 빔이 나갔다. 다른 사고는 1바이트 공유 카운터가 넘쳐 0이 되는 순간 안전 검사가 건너뛰어진 것이 원인이었다. 이전 기종에 있던 하드웨어 인터록을 없애고 소프트웨어에만 의존한 점도 지적되었다.[10]
2003년 북미 대정전 2003년 8월 14일 미국 북동부와 캐나다 온타리오에서 약 5,000만 명이 정전을 겪었다. 미국·캐나다 합동 조사단 보고서는 오하이오 전력 회사 FirstEnergy의 제어실에서 GE XA21 에너지 관리 시스템의 경보·로그 기능이 14시 14분부터 멈췄고, 운영자들이 이를 한 시간 넘게 모른 채 송전선 고장에 대응하지 못한 것을 주요 원인으로 들었다.[11] 이후 GE와 조사진이 시스템을 느리게 돌려 재현한 끝에 경보 처리 소프트웨어의 경쟁 상태를 원인으로 찾아낸 것으로 알려져 있다.[12]
Dirty COW(CVE-2016-5195) 리눅스 커널 메모리 관리의 쓰기 시 복사(copy-on-write) 처리에 있던 경쟁 상태다. 권한 없는 로컬 사용자가 읽기 전용 메모리 매핑에 쓸 수 있게 되어, 읽기만 가능한 SetUID 바이너리나 /etc/passwd 같은 파일의 페이지 캐시를 고쳐 root 권한을 얻을 수 있었다. 2007년 커널 2.6.22부터 있던 버그로 2016년 10월 공개·수정되었고, 공개 전에 이미 실제 공격에 쓰인 흔적이 발견되었다.[13][14]

경쟁 상태는 실행 순서가 비결정적이라 디버거를 붙이거나 로그를 추가하면 타이밍이 바뀌어 사라지기도 한다(하이젠버그, heisenbug). 그래서 설계 단계에서 피하는 것이 가장 좋고, 도구는 보조 수단이다.

도구 방식 사용법
ThreadSanitizer(TSan) 컴파일 시 계측, 실행 중 happens-before 분석. C, C++, Go, Rust 지원. 속도 5~15배 느려지고 메모리 5~10배 사용[15] clang -fsanitize=thread -g -O1(GCC도 같은 옵션)
Go 경쟁 탐지기 TSan 기반. 실제로 실행된 경로의 데이터 경쟁만 찾는다. 메모리 5~10배, 시간 2~20배[2] go test -race, go run -race, go build -race
Helgrind(Valgrind) 바이너리 계측. pthread API 오용, 락 순서 문제(잠재적 교착), 데이터 경쟁을 찾는다[16] valgrind --tool=helgrind ./prog
DRD(Valgrind) Helgrind와 비슷한 데이터 경쟁 탐지기 valgrind --tool=drd ./prog
Clang Thread Safety Analysis 정적 분석. 변수에 "이 락으로 보호" 주석을 달면 컴파일 때 검사 -Wthread-safety
Intel Inspector 메모리·스레드 오류 동적 분석(C, C++, Fortran) GUI/명령줄
KCSAN 리눅스 커널용 동시성 버그 탐지기 커널 설정 CONFIG_KCSAN
Java 도구 jcstress(동시성 스트레스 시험), SpotBugs의 동기화 검사

정적 분석과 모델 검사(TLA+, SPIN)는 가능한 모든 순서를 따져 보고, 스트레스 시험은 스레드 수와 반복 횟수를 늘리고 일부러 sleep이나 양보를 넣어 드문 순서를 끌어낸다. 웹 서비스는 Burp Suite Repeater의 병렬 전송(단일 패킷 공격) 기능으로 한도 초과를 시험한다.