본문 바로가기
카테고리 없음

프론트 개발자가 보는 보안 뉴스 #01 - CVE-2026-98432

by 백씨네 2026. 10. 30.
728x90

메모리를 해제했는데, 왜 다시 읽을 수 있을까?

2026년 10월 15일, 리눅스 커널에서 새로운 보안 취약점인 CVE-2026-98432가 공개됐다.

TCP 연결을 처리하는 과정에서 발생하는 메모리 관련 취약점으로, Ubuntu를 포함한 여러 리눅스 환경에 영향을 미치는 문제다.

취약점에 대한 설명을 읽다 보니 다음과 같은 내용이 있었다.

RCU(Read-Copy-Update) 유예 기간이 지나기 전에 메모리를 해제하면서,
다른 CPU가 이미 해제된 메모리에 접근하는 Use-After-Free 취약점이 발생한다.

 

처음에는 조금 의아했다.

"메모리를 해제했으면 데이터가 사라져야 하는 거 아닌가? 그런데 어떻게 다시 접근한다는 거지?"

JavaScript에서는 가비지 컬렉터(GC)가 사용하지 않는 메모리를 정리한다고 알고 있었는데, 리눅스 커널은 다른 방식으로 동작하는 걸까?

궁금해서 조금 더 찾아봤다.


1. 메모리를 해제한다고 데이터가 즉시 사라지는 건 아니다

먼저 메모리를 해제한다는 것이 정확히 어떤 의미인지 알아볼 필요가 있었다.

이해하기 쉽게 호텔 객실에 비유해 보자.

 

호텔에 101호 객실이 있고, 투숙객 A가 사용하고 있다고 가정하자.

A가 체크아웃하면 호텔에서는 101호를 빈 객실로 등록한다.
그런데 체크아웃했다고 해서 방 안에 있던 모든 물건이 그 순간 사라지는 것은 아니다.

기존 물건이 남아 있을 수도 있고, 다른 투숙객이 들어와 완전히 바뀌었을 수도 있다.

 

메모리를 해제하는 것도 비슷한 개념이다.

C언어에서 사용하는 free(), 리눅스 커널에서 사용하는 kfree()는 해당 메모리를 더 이상 사용하지 않겠다고 메모리 관리자에게 반환하는 작업이다.

 

그렇다고 해서 반드시 해당 주소에 저장된 모든 데이터가 즉시 0으로 지워지는 것은 아니다.

여기서 중요한 점이 있다.

 

메모리를 해제했다는 것은 데이터가 사라졌다는 뜻이 아니라, 더 이상 그 메모리에 접근해서는 안 된다는 뜻이다.

 

기존 데이터가 남아 있을 수도 있지만, 이미 다른 데이터로 덮어써졌거나 접근할 수 없는 상태가 됐을 수도 있다.

그렇다면 해제된 메모리에 어떻게 다시 접근할 수 있는 걸까?

해제된 메모리를 가리키는 주소는 남아 있을 수 있다

C언어에서는 메모리 주소를 가리키는 포인터(Pointer) 라는 개념을 사용한다.

포인터는 쉽게 말해 데이터가 저장된 위치를 가리키는 주소 정보다.

 

문제는 free()나 kfree()를 호출하더라도 그 주소를 보관하던 모든 포인터 변수가 자동으로 비워지는 것은 아니라는 점이다.

다른 코드가 기존 주소를 가지고 있다면, 이미 해제된 메모리에 접근을 시도할 수 있다.

또한 메모리를 해제했다고 해서 해당 주소에 대한 접근이 반드시 즉시 차단되는 것도 아니다.

그래서 이전 데이터가 읽히는 것처럼 보이거나, 전혀 다른 데이터가 읽히거나, 프로그램이 충돌할 수도 있다.

C언어에서는 이를 정의되지 않은 동작(Undefined Behavior) 이라고 한다.

즉, 어떤 결과가 발생할지 보장할 수 없다는 뜻이다.

 

이처럼 이미 해제된 메모리에 다시 접근하는 문제를 Use-After-Free(UAF) 라고 부른다.

 

그렇다면 개발자에게 익숙한 JavaScript에서는 어떨까?


2. JavaScript에서는 왜 이런 문제가 잘 발생하지 않을까?

JavaScript에는 가비지 컬렉션(Garbage Collection, GC)이라는 자동 메모리 관리 기능이 있다.

더 이상 접근할 수 없는 객체를 찾아 메모리를 회수하는 방식이다.

간단한 코드를 살펴보자.

const requests = new Map();

requests.set("request-1", {
  status: "pending",
});

const request = requests.get("request-1");

requests.delete("request-1");

console.log(request.status); // "pending"

 

requests.delete()를 호출했으니 데이터가 삭제됐을 것 같지만, request.status는 여전히 정상적으로 출력된다.

왜 그럴까?

 

Map에서 데이터를 삭제했더라도 request라는 변수가 여전히 해당 객체를 참조하고 있기 때문이다.

정확히는 Map에 등록된 항목을 제거했을 뿐, 객체 자체가 즉시 사라진 것은 아니다.

JavaScript의 GC는 일반적으로 객체를 참조하는 경로가 남아 있다면 해당 객체를 회수하지 않는다.

여기서 C언어와의 중요한 차이가 나타난다.

 

JavaScript에서는 GC가 메모리 회수 시점을 자동으로 관리하지만, C언어에서는 개발자가 직접 메모리를 해제할 수 있다.

문제는 다른 코드가 해당 메모리를 사용하고 있는 상황에서도 잘못된 시점에 해제가 이루어질 수 있다는 점이다.

 

이미 해제된 메모리를 다른 코드가 계속 사용하려 한다면 UAF가 발생할 수 있다.

물론 JavaScript에서도 메모리 누수 등 여러 메모리 관리 문제가 발생할 수 있다.
다만 일반적인 JavaScript 객체는 C언어의 포인터처럼 해제된 메모리 주소에 직접 접근하는 방식으로 사용하지 않는다.

앞의 JavaScript 코드는 UAF 취약점을 재현한 코드가 아니다.

 

객체를 참조하는 변수가 남아 있어서 정상적으로 접근할 수 있는 상황을 보여주는 예시다.


3. 그렇다면 이번 리눅스 취약점에서는 무슨 일이 발생했을까?

이번 취약점은 TCP 연결을 처리하는 과정에서 발생했다.

TCP는 서버와 연결을 시작할 때 다음과 같은 과정을 거친다.

  1. SYN: 클라이언트가 서버에 연결을 요청한다.
  2. SYN-ACK: 서버가 연결 요청을 확인하고 응답한다.
  3. ACK: 클라이언트가 응답을 확인한다.

이 과정을 3-Way Handshake라고 부른다.

그런데 실제 서버에서는 여러 CPU가 동시에 네트워크 요청을 처리할 수 있다.

예를 들어 CPU A가 어떤 TCP 연결에 관한 메모리를 정리하는 동안, CPU B는 같은 연결과 관련된 정보를 읽고 있을 수 있다.

이번 취약점에서도 비슷한 일이 발생했다.

문제는 옵션 메모리의 해제 시점이었다

이번 취약점의 한 발생 경로에서는 TCP 연결 요청이 보관하던 IPv4 옵션 메모리의 소유권을 새로운 소켓으로 넘겼다.

이 과정에서 옵션 데이터를 별도로 복사하지는 않았다.

그런데 다른 CPU는 재전송된 SYN 요청에 응답하기 위해 같은 옵션 정보를 여전히 읽고 있을 수 있었다.

그 사이 새로운 소켓이 해제되면서 옵션 메모리까지 바로 해제되면 어떻게 될까?

다른 CPU는 아직 읽고 있던 메모리가 해제됐다는 사실을 모른 채 접근하게 된다.

여기서 문제가 되는 개념은 두 가지다.

  • Race Condition(경쟁 조건): 여러 작업의 실행 순서나 타이밍에 따라 결과가 달라지는 문제
  • Use-After-Free(UAF): 이미 해제된 메모리에 다시 접근하는 문제

조금 더 쉽게 상황을 정리해 보자.

정상적인 상황

CPU B: "이 데이터 아직 읽고 있어."
CPU A: "그럼 안전하게 읽기가 끝날 때까지 해제하지 않을게."
CPU B: "다 읽었어."
CPU A: "이제 메모리를 해제할게."

 

문제가 발생하는 상황

CPU B: "이 데이터 아직 읽고 있어."
CPU A: "이제 필요 없는 데이터네. 해제할게."
CPU B: "아직 읽는 중인데? 어? 데이터가 이상한데?"

 

그렇다면 여러 CPU가 동시에 작업하는 환경에서는 이런 문제를 어떻게 예방할까?


4. RCU는 무엇이고, 유예 기간은 왜 필요할까?

리눅스 커널에서는 여러 작업이 동시에 데이터를 읽는 상황을 안전하게 처리하기 위해 RCU(Read-Copy-Update) 라는 동기화 기술을 사용한다.

RCU의 핵심 아이디어는 간단하다.

 

"데이터가 더 이상 필요하지 않더라도, 기존에 읽고 있던 작업이 안전하게 끝나기 전까지는 메모리를 회수하지 말자."

 

다시 호텔 객실을 생각해 보자.

호텔에서 101호를 폐쇄하기로 결정했다고 가정하자.
그런데 청소 직원이 아직 방 안에서 작업하고 있다.

이때 호텔이 객실을 폐쇄 대상으로 등록하는 것은 가능하다.

하지만 직원이 작업을 끝내기도 전에 객실을 철거해 버리면 문제가 생긴다.

따라서 기존에 객실을 사용하던 직원이 나올 때까지 기다린 후 철거해야 한다.

RCU도 이와 비슷하다.

데이터 제거와 실제 메모리 해제는 다르다

RCU는 데이터를 기존 자료구조에서 제거하는 것과 실제 메모리를 회수하는 시점을 구분한다.

먼저 새로운 읽기 작업이 제거된 데이터에 접근하지 못하도록 한다.

그다음 기존에 해당 데이터를 참조할 수 있었던 RCU 읽기 구간들이 모두 끝날 때까지 메모리 회수를 미룬다.

이를 보장하기 위해 필요한 기간을 유예 기간(Grace Period) 이라고 한다.

여기서 유예 기간은 단순히 정해진 몇 밀리초를 기다리는 타이머가 아니다.

기존에 데이터를 읽고 있던 작업들이 안전한 상태에 도달했음을 보장하기 위한 과정이다.

이번 취약점은 바로 이 지점에서 발생했다.

기존에 데이터를 읽는 코드에서는 RCU를 사용하고 있었지만, 일부 메모리 해제 경로에서는 RCU 유예 기간을 고려하지 않고 kfree()로 메모리를 해제하고 있었다.

즉, 읽는 쪽은 RCU 읽기 구간 안에서 데이터를 사용했지만, 해제하는 쪽이 유예 기간 전에 메모리를 해제해 읽기가 끝날 때까지 데이터가 유지되지 못한 것이다.


5. 그런데 이게 왜 보안 취약점일까?

해제된 메모리에 접근하면 다른 용도로 재사용된 데이터를 기존 객체의 데이터로 잘못 해석할 수 있다.

이런 문제는 프로그램 충돌을 일으킬 수 있고, 메모리를 읽거나 쓰는 방식에 따라 메모리 손상으로 이어질 수도 있다.

 

특히 이번 취약점은 외부에서 전달되는 TCP 연결 요청을 처리하는 경로에서 발생한다.

따라서 원격 공격자가 인증 없이 특정한 타이밍의 네트워크 요청을 이용해 문제를 유발할 가능성이 있다.

Red Hat의 보안 설명에서는 주요 위험으로 시스템 충돌에 따른 서비스 거부(DoS) 와 잠재적인 메모리 손상을 언급하고 있다.

여기서 서비스 거부란 정상적인 사용자가 서버를 이용하지 못하도록 만드는 공격을 의미한다.

예를 들어 커널이 충돌해 서버가 정상적으로 응답하지 못한다면, 해당 서버에서 제공하는 서비스에도 장애가 발생할 수 있다.

다만 한 가지 주의할 점이 있다.

 

메모리 손상이 발생할 가능성이 있다는 사실과 실제 원격 코드 실행(RCE)이 입증됐다는 것은 다르다.

Red Hat은 이번 경쟁 조건을 악용하기 어렵다고 평가하며, 기밀성이나 무결성에 대한 실제 영향은 입증되지 않았다고 설명한다.

따라서 취약점의 잠재적인 위험과 현재까지 확인된 영향을 구분해서 이해할 필요가 있다.


6. 그럼 어떻게 해결할까?

이번 문제에 대한 Linux 커널의 수정 방향은 비교적 명확하다.

기존에는 일부 경로에서 메모리를 kfree()로 바로 해제하고 있었다.

이를 RCU 유예 기간이 지난 이후 메모리를 회수하도록 예약하는 kfree_rcu() 방식으로 변경했다.

두 함수의 차이를 간단히 정리하면 다음과 같다.

  • kfree(): 해당 메모리를 해제한다.
  • kfree_rcu(): RCU 유예 기간이 지난 후 메모리를 해제하도록 예약한다.

즉, 기존 읽기 작업이 아직 메모리를 사용하고 있는 상황에서 해당 메모리가 너무 일찍 회수되지 않도록 수정한 것이다.

다만 실제 서버에서의 해결 방법은 Linux 커널 버전과 배포판에 따라 달라진다.

Linux 커널에 수정 사항이 반영됐더라도 Ubuntu와 같은 배포판에서는 별도의 패키지 업데이트나 백포트가 적용될 수 있기 때문이다.

따라서 특정 명령어를 일괄적으로 안내하기보다는 공식 보안 공지에서 자신의 환경에 해당하는 패치 상태를 확인하는 것이 더 정확하다.

2026년 10월 30일 확인한 Ubuntu 공식 CVE 페이지에서는 일반 커널인 linux 패키지 기준으로 다음 버전을 취약한 상태이면서 수정 작업이 진행 중인 것으로 표시하고 있다.

  • Ubuntu 26.10
  • Ubuntu 26.04 LTS
  • Ubuntu 24.04 LTS
  • Ubuntu 22.04 LTS

Ubuntu 20.04 LTS 역시 취약한 상태로 표시되어 있다.

해당 페이지의 최종 갱신일은 2026년 10월 29일이며, 실제 적용 가능한 수정 버전과 패키지 상태는 이후 변경될 수 있다.

관련 공식 문서


마무리

처음 이 뉴스를 접했을 때는 단순한 의문이었다.

"이미 메모리를 해제했는데 어떻게 다시 읽을 수 있지?"

그런데 하나씩 살펴보니 메모리를 해제한다는 것이 데이터를 즉시 삭제한다는 의미는 아니라는 사실을 알게 됐다.

또한 JavaScript의 GC와 C언어의 메모리 관리 방식이 어떻게 다른지도 조금 더 이해할 수 있었다.

이번 취약점을 통해 가장 중요하게 느낀 점은 이것이다.

메모리를 안전하게 관리하려면 언제 할당하고 해제하는지만큼, 누가 아직 그 메모리를 사용하고 있는지도 중요하다.

평소 프론트엔드를 개발하면서 메모리를 직접 해제할 일은 많지 않다.

하지만 우리가 작성한 코드 역시 브라우저, 런타임, 운영체제 위에서 실행된다.

이런 보안 뉴스를 통해 평소에는 신경 쓰지 않았던 시스템의 동작 원리를 조금씩 이해해 보는 것도 의미 있는 공부가 될 것 같다.

반응형