본문 바로가기

[News]

DNS 루트 KSK 교체·Bun AOT·C2y — 이번 주 IT 뉴스

반응형

어제(10월 11일) 인터넷의 맨 꼭대기 열쇠가 바뀌었습니다. DNS 루트 존이 이제 새 키 KSK-2024 하나로만 서명됩니다. 이번 주에는 이 소식 말고도 Bun 의 TypeScript AOT 컴파일 실험, 셸 설정 한 줄에 숨은 PATH 함정, C 표준의 정의되지 않은 동작(UB) 정리, Rust 와 다른 방식의 빌림 검사기, 작은 언어 모델로 "고르기만 하는" 결정 모델을 만드는 방법이 올라왔습니다. 개발자가 직접 확인하거나 써 볼 만한 것만 여섯 개 추렸습니다.

■ 오늘 소식 한눈에

소식 | 분야 | 한 줄 요약 | 영향받는 사람
----------------------------------------------------
루트 KSK 교체 완료 | 인프라·보안 | 10월 11일부터 루트 존이 KSK-2024(키 태그 38696)로만 서명됩니다 | DNSSEC 검증 리졸버를 직접 운영하는 사람
Bun --aot 실험 | 언어·런타임 | TS 타입을 근거로 네이티브 코드를 미리 생성하고, 타입 단언을 런타임 검사로 바꿉니다 | Bun 으로 단일 실행 파일을 배포하는 팀
PATH 안의 ~ | 개발 환경·보안 | 따옴표 안의 ~ 는 펼쳐지지 않아 상대 경로가 PATH 에 들어갑니다 | dotfile 을 손으로 관리하는 모든 사람
C2y 의 UB 정리 | 언어 | 표준의 UB 약 100개 중 45개를 C2y 초안에서 없앴습니다 | C 코드베이스 유지보수자, 임베디드·커널 개발자
Valen 그룹 빌림 | 언어 연구 | "공유 XOR 가변" 대신 "해제 후 사용 금지"만 강제하는 빌림 검사기 | Rust 빌림 검사기에 막혀 본 사람
작은 LLM 결정 모델 | AI·ML | Qwen3-1.7B 로 보기 몇 개 중 하나를 한 번의 순전파로 고르고 확률을 보정합니다 | 분류·라우팅에 LLM 을 쓰는 백엔드 개발자

■ 1. DNS 루트 키가 바뀌었습니다 — KSK-2017 에서 KSK-2024 로

▶ 무슨 일인가

ICANN 공지에 따르면 2026년 10월 11일부터 루트 존은 KSK-2024 만으로 서명됩니다. KSK 는 Key Signing Key 의 줄임말로, 루트 존의 DNSKEY 레코드 묶음에 서명하는 키입니다. 옛 키 KSK-2017 의 키 태그는 20326, 새 키 KSK-2024 의 키 태그는 38696 입니다. APNIC 블로그의 Geoff Huston 글은 이번 교체가 알고리즘 변경이 아니라 키 쌍만 바꾸는 것이라고 짚습니다.

IANA 는 2024년 7월 새 키를 공개했고 2025년 1월 11일 루트 존에 게시했습니다. 옛 키 폐기(revoke)는 2027년 1월 11일로 운영자 브리핑에 잡혀 있습니다.

▶ 동작 원리: 왜 루트 키 하나에 인터넷 전체가 걸리는가

DNSSEC 검증은 위에서 아래로 내려가는 서명 사슬입니다.

1. 검증 리졸버는 루트 KSK 의 공개키를 신뢰 앵커로 미리 갖고 있습니다. BIND 는 bind.keys, Unbound 와 PowerDNS Recursor 는 root.key, Knot Resolver 는 root.keys 파일입니다.
2. 리졸버가 루트의 DNSKEY 묶음을 받아 그 서명(RRSIG)을 신뢰 앵커로 검증합니다. 이 묶음에는 실제 레코드에 서명하는 ZSK 가 들어 있습니다.
3. ZSK 로 루트 존의 DS 레코드를 검증하고, DS 로 .com 같은 TLD 의 키를 검증하며 아래로 내려갑니다.

루트에는 부모가 없습니다. 그래서 새 키를 믿게 할 방법이 "옛 키가 새 키에 서명하는 것"뿐입니다. 2025년 1월부터 루트 DNSKEY 묶음에 두 키가 함께 실렸고, 옛 키가 이 묶음에 서명했습니다. RFC 5011 을 따르는 리졸버는 옛 키로 검증된 묶음에서 새 키를 발견하면 바로 믿지 않고 보류 기간(hold-down)을 둡니다. 기간은 30일과 새 키가 처음 담긴 묶음의 원래 TTL 중 큰 쪽입니다. 그동안 새 키가 담긴 검증된 묶음을 두 번 이상 보면 신뢰 앵커에 추가합니다.

[이미지 01] 01-ksk-rollover.png


(루트 KSK 교체와 검증 사슬)

문제는 2단계입니다. 서명이 새 키로만 이뤄지는 지금, 앵커에 38696 이 없는 리졸버는 루트 DNSKEY 묶음부터 검증에 실패합니다. 그 아래 모든 서명이 따라서 무너집니다. ICANN 은 이 상황을 "total DNS resolution failures" 라고 적었습니다.

▶ 얼마나 준비됐나

측정 | 출처 | 결과
------------------------
보고하는 리졸버 중 KSK-2024 인식 | ICANN 블로그 | 95% 이상
보고하는 리졸버 중 신뢰 앵커에 추가(2026년 7월) | Verisign 데이터(APNIC 글 인용) | 약 90%
RIPE Atlas 프로브(10월 5일) | APNIC 글 | 새 키 적재와 일치 1,230개, 불일치 47개
광고 기반 RFC 8509 측정 | APNIC 글 | 명확한 신호 하루 약 180만 건 중 약 70% 가 새 키 적재

▶ 확인할 것

사내 DNS 를 직접 돌린다면 지금 바로 앵커 파일을 봅니다. 키 태그 38696 이 없으면 자동 갱신(RFC 5011)이 켜져 있는지, 리졸버가 그 파일이 있는 디렉터리에 쓸 권한이 있는지 확인합니다.

리졸버가 RFC 8509 센티널을 지원하면 질의 한 번으로도 확인됩니다. root-key-sentinel-is-ta-<키태그> 로 시작하는 이름을 DNSSEC 서명된 도메인 아래에 붙여 묻습니다. 그 키를 신뢰하면 원래 답이 오고, 신뢰하지 않으면 SERVFAIL 이 옵니다.

[코드: bash]
# 루트 존에 실린 KSK 확인 (flags 257 이 KSK, 주석의 id 가 키 태그)
dig . DNSKEY +multi | grep -E 'KSK|id = '

# 내 리졸버(예: 10.0.0.53)가 KSK-2024 를 신뢰하는지 — 센티널 질의
dig @10.0.0.53 root-key-sentinel-is-ta-38696.isc.org A +short
dig @10.0.0.53 root-key-sentinel-not-ta-38696.isc.org A +short
[/코드]

is-ta 는 답이 나오고 not-ta 가 SERVFAIL 이면 정상입니다. 두 질의가 같은 결과를 내면 리졸버가 센티널을 지원하지 않는 것이니, 그때는 앵커 파일을 직접 봅니다.

[이미지 02] 02-shot-2.png


(APNIC 블로그의 루트 키 교체 측정 글 (출처: APNIC Blog))

■ 2. Bun, TypeScript 를 네이티브 코드로 미리 컴파일하는 --aot 실험

▶ 무슨 일인가

Bun 팀이 X(트위터)에 bun build --compile --aot 실험 결과를 공개했습니다. 기준은 Bun 1.4 입니다. 지금까지 Bun 의 배포 방식은 둘이었습니다. --compile 은 JS 번들과 Bun 런타임을 실행 파일 하나로 묶습니다. --bytecode 는 JavaScriptCore 바이트코드를 미리 만들어 둡니다. 둘 다 결국 JS 엔진이 실행합니다. --aot 는 함수를 네이티브 코드로 미리 만듭니다. 공개된 글에는 성능 수치가 없습니다.

▶ 동작 원리: 타입이 "검사용 주석"에서 "실행 계약"으로 바뀝니다

평소 TypeScript 타입은 실행 전에 지워집니다. AOT 컴파일러가 빠른 기계어를 만들려면 "이 변수는 반드시 number" 라는 사실이 필요합니다. Bun 의 실험은 타입을 그 근거로 씁니다.

- 컴파일 시점에 알 수 있는 타입 오류는 빌드를 거부합니다. 실행 파일이 아예 나오지 않습니다.
- JSON.parse() 결과처럼 컴파일 시점에 모르는 값은 경계에서 런타임 검사를 넣습니다. 맞지 않으면 TypeError 를 던집니다.
- as 와 ! 는 지워지지 않고 검사나 실제 형변환이 됩니다.
- Object.prototype 수정, 내장 메서드 교체, 사용자 클래스 프로토타입 메서드 교체를 막습니다. 객체 모양과 호출 대상이 실행 중에 바뀌지 않는다고 가정해야 호출을 직접 연결할 수 있기 때문입니다.

▶ 왜 중요한가: 같은 코드가 다른 값을 냅니다

발표에 나온 예가 핵심을 보여 줍니다.

[코드: typescript]
// semantics.ts — 지금의 bun/node 에서 실행하면 "51" 이 찍힙니다
const raw: unknown = JSON.parse('"5"');
const n = (raw as number) + 1;
console.log(n);
[/코드]

[코드: bash]
bun run semantics.ts   # 51 — as 는 지워지고, "5" + 1 은 문자열 연결
[/코드]

발표에 따르면 --aot 빌드에서는 같은 식이 6 을 냅니다. as number 가 실제 숫자 변환으로 바뀌기 때문입니다. 팀도 완전한 하위 호환을 약속하지 않습니다.

▶ 써 볼 것

아직 실험 플래그입니다. 당장 프로덕션에 쓸 단계는 아닙니다. 대신 지금 코드베이스에서 as 와 ! 가 몇 개인지, any 를 거쳐 들어오는 값이 어디인지 세어 보면 좋습니다.

■ 3. PATH 안의 ~ 는 홈이 아닐 수 있습니다

▶ 무슨 일인가

disconnect3d 의 글은 아주 흔한 설정 한 줄을 다룹니다.

[코드: bash]
export PATH="$PATH:~/.local/bin/"
[/코드]

이 줄은 홈 디렉터리의 .local/bin 을 추가하지 않습니다. 현재 디렉터리 아래의 ~/.local/bin/ 이라는 상대 경로를 추가합니다.

▶ 동작 원리: 물결표 확장은 셸이 하고, 따옴표 안에서는 안 합니다

~ 를 홈 경로로 바꾸는 일은 커널도, execvp 도 아닌 셸의 단어 확장 단계에서 일어납니다. bash 매뉴얼은 "단어가 따옴표로 감싸지 않은 물결표로 시작하면" 확장한다고 적습니다. bash 와 zsh 는 변수 대입에서 = 뒤와 각 : 뒤의 물결표도 확장합니다. 그래서 따옴표가 없는 PATH=$PATH:~/.local/bin 은 동작합니다. 큰따옴표로 감싸는 순간 ~ 는 그냥 글자입니다.

명령을 찾을 때는 PATH 를 : 로 나눈 항목마다 항목/명령이름 을 붙여 봅니다. 항목이 / 로 시작하지 않으면 현재 작업 디렉터리 기준으로 풀립니다. 결국 cd 한 곳마다 다른 실행 파일을 고를 수 있게 됩니다.

[이미지 03] 03-tilde-path.png


(따옴표 안 물결표가 PATH 에 남는 흐름)

글쓴이의 재현은 이렇습니다. 빈 디렉터리에서 따라 할 수 있습니다.

[코드: bash]
cd "$(mktemp -d)"
mkdir -p './~/.local/bin'
printf '#!/bin/sh\necho hello\n' > './~/.local/bin/kek'
chmod +x './~/.local/bin/kek'
PATH="~/.local/bin/" kek    # hello — 홈은 한 번도 쓰이지 않았습니다
[/코드]

▶ 왜 중요한가

평소에는 그 상대 경로에 아무것도 없으니 증상이 없습니다. 그러나 남이 만든 저장소를 클론하고 그 안에서 명령을 치는 일은 흔합니다. 저장소에 ~ 라는 디렉터리가 들어 있고 그 안에 흔한 명령 이름의 실행 파일이 있다면, PATH 앞쪽 항목에서 못 찾은 명령이 그쪽으로 넘어갈 수 있습니다.

▶ 확인할 것

[코드: bash]
echo "$PATH" | tr ':' '\n' | grep -n '~'
[/코드]

한 줄이라도 나오면 ~/.zshrc, ~/.bashrc, ~/.profile 에서 ~ 를 $HOME 으로 바꿉니다.

■ 4. C2y 초안, UB 약 100개 중 45개를 없앴습니다

▶ 무슨 일인가

LWN 은 Kernel Recipes 2026 에서 C 표준 위원회 위원 Martin Uecker 가 한 발표를 정리했습니다. C 표준에는 정의되지 않은 동작이 약 100가지 있습니다. 진행 중인 C2y 초안은 그중 45개를 없앴습니다. C2y 에는 case 범위, 이름 붙은 for 루프, 배열 길이를 구하는 _Countof() 가 들어갈 예정입니다.

▶ 동작 원리: "시간 역행" 최적화

UB 가 왜 무서운지는 발표의 예제가 잘 보여 줍니다.

[코드: c]
extern int x;

int f(int a, int b)
{
    x = b ? 42 : 43;
    return a / b;
}
[/코드]

b == 0 이면 나눗셈이 UB 입니다. 컴파일러는 "UB 는 일어나지 않는다"고 가정해도 되니 b 는 0 이 아니라고 보고, 조건 검사를 지운 뒤 x = 42 만 남길 수 있습니다. 여기까지는 x 에 저장하는 것이 관찰 가능한 동작이 아니어서 허용됩니다.

x = ... 대신 g(b ? 42 : 43); 를 부르면 이야기가 다릅니다. g() 가 b == 0 일 때 exit() 를 부르면 나눗셈은 영영 실행되지 않습니다. UB 가 없는 실행인데도 g(42) 로 바꿔 버리면 컴파일러 버그입니다. 발표에 따르면 실제로 이런 최적화를 한 컴파일러가 있었습니다. volatile 변수에 저장한 다음 나눗셈을 하는 코드에서 나눗셈을 저장 앞으로 끌어올린 사례도 있었습니다. 뒤에 올 UB 가 앞선 관찰 가능한 동작까지 바꾸는 셈입니다. C23 은 이를 막는 "시간 역행 금지" 조항을 넣었습니다.

▶ 왜 중요한가

UB 가 하나 줄 때마다 "컴파일러가 마음대로 해도 되는 경우"가 하나 줄어듭니다. 하지만 발표의 결론은 냉정합니다. 공간 안전성(경계 검사)은 counted_by 속성과 컴파일러 경고로 부분적으로 풀리고 있지만, 시간 안전성(해제 후 사용)은 가장 어렵고 Rust 가 분명히 앞섭니다. C2y 로 완전한 메모리 안전성에 이르지는 못합니다.

▶ 써 볼 것

유연한 배열 멤버를 쓰는 구조체가 있다면 counted_by 를 붙일 수 있는지 봅니다. 발표는 이 속성이 유연한 배열 멤버의 경계 검사를 가능하게 한다고 설명합니다. 사용 중인 컴파일러 버전의 문서에서 지원 여부를 먼저 확인합니다.

■ 5. Valen 의 그룹 빌림 — "공유 XOR 가변"을 버린 빌림 검사기

▶ 무슨 일인가

Evan Ovadia(verdagon)가 실험 언어 Valen 과 그 빌림 검사기를 공개했습니다. 설계는 Nick Smith 의 그룹 빌림(group borrowing) 에서 왔습니다. 목표는 가비지 컬렉터 없는 메모리 안전성입니다. Rust 와 같은 목표를 다른 규칙으로 노립니다.

▶ 동작 원리: 참조가 "어디를 가리키는지"를 기억합니다

Rust 의 규칙은 "공유 참조 여럿 또는 가변 참조 하나"입니다. Valen 은 이 규칙을 버리고 "무효화된 참조를 쓰지 말 것" 하나만 강제합니다.

1. 모든 객체는 하나의 소유자(객체, 배열, 스택 프레임, 전역)가 있습니다. 그래서 모든 객체에 world.entities[] 같은 경로가 하나씩 생깁니다.
2. 참조는 자기가 가리키는 경로를 기억합니다.
3. 어떤 객체를 수정하거나 파괴하면 그 안쪽을 가리키는 참조만 무효가 됩니다.
4. 무효가 된 참조를 쓰면 컴파일 오류입니다.
5. 함수 시그니처가 어떤 경로를 수정하는지 밝힙니다. 그래서 호출자는 무엇이 무효가 될지 압니다.

원문 예제를 옮기면 이렇습니다(Valen 문법).

[코드: text]
let vec = Vec<Ship>.new();
vec.push(Ship(42));
let vec_ref = &vec;
let elem_ref = &vec[0];
vec.push(Ship(73));      // 벡터 내부가 바뀜 → elem_ref 무효, vec_ref 는 유효
vec_ref.push(Ship(81));  // 문제없음
return elem_ref.fuel;    // 컴파일 오류
[/코드]

Rust 라면 vec_ref 와 elem_ref 를 동시에 쥐는 순간 막힙니다. Valen 은 push 가 실제로 망가뜨리는 것, 곧 원소를 가리키는 참조만 막습니다. world.entities[] mut 처럼 함수가 엔티티 하나만 고친다고 선언하면, 호출자가 쥔 다른 엔티티 참조는 살아남습니다.

▶ 확인할 것

아직 연구 단계입니다. 원문은 타입 안정성 예외, 다형 그룹·런타임 그룹, 참조 카운팅, Rust 연동을 "앞으로 넣을 것"으로 적었습니다.

[이미지 04] 04-shot-3.png


(Valen 그룹 빌림 소개 글 (출처: verdagon.dev))

■ 6. 작은 LLM 으로 "고르기만 하는" 결정 모델 만들기

▶ 무슨 일인가

Nish Tahir 의 글(해커뉴스 402점)은 거대한 생성 대신 정해진 보기 중 하나를 고르는 모델을 만듭니다. 바탕은 Qwen/Qwen3-1.7B 입니다. 평가는 CommonsenseQA 에서 무작위로 떼어 낸 1,221문항으로 합니다.

▶ 동작 원리: 생성하지 않고 logits 만 봅니다

토큰을 하나씩 뽑아 문장을 만들 필요가 없습니다. 질문과 보기 AE 를 프롬프트에 넣고 순전파를 한 번 합니다. 마지막 위치의 logits 에서 AE 다섯 토큰만 골라 softmax 를 씌우면 다섯 보기의 확률 분포가 나옵니다. 나머지 어휘는 마스킹으로 버립니다.

[이미지 05] 05-decision-model.pn

g
(결정 모델의 한 번 순전파와 온도 보정)

[코드: python]
# pip install torch transformers
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

name = "Qwen/Qwen3-1.7B"
tok = AutoTokenizer.from_pretrained(name)
model = AutoModelForCausalLM.from_pretrained(name, torch_dtype="auto")
model.eval()

question = "Where would you most likely find a bat?"
# 보기는 예시입니다. 원문은 CommonsenseQA 를 씁니다.
options = ["Cave", "Kitchen", "Library", "Ocean", "Desert"]
letters = ["A", "B", "C", "D", "E"]
body = question + "\n" + "\n".join(f"{l}. {o}" for l, o in zip(letters, options))

prompt = tok.apply_chat_template(
    [{"role": "user", "content": body + "\nAnswer with a single letter."}],
    tokenize=False, add_generation_prompt=True, enable_thinking=False,
)
ids = tok(prompt, return_tensors="pt")
option_ids = [tok.encode(l, add_special_tokens=False)[0] for l in letters]

with torch.no_grad():
    logits = model(**ids).logits[0, -1]

T = 1.0  # 보정 후에는 검증 세트에서 맞춘 값으로 바꿉니다
probs = torch.softmax(logits[option_ids].float() / T, dim=-1)
for l, o, p in zip(letters, options, probs.tolist()):
    print(f"{l}. {o:8s} {p:.4f}")
[/코드]

▶ 결과와 보정

단계 | 정확도 | 매크로 F1
----------------------------------
바탕 모델 | 725/1221 = 0.5938 | 0.5844
짧은 파인튜닝 후 | 762/1221 = 0.6241 | 0.6234

정확도보다 흥미로운 건 보정(calibration) 입니다. 모델은 지나치게 확신했습니다. 애매한 질문 "박쥐를 어디서 가장 볼 법한가?"에 Cave 를 0.9978 로 답했고, 확신도 0.91.0 구간의 실제 정확도는 약 70% 였습니다. 글쓴이는 온도 스케일링으로 이를 고칩니다. logits 를 상수 T 로 나눈 뒤 softmax 를 씌우는 방법이고, 검증 세트에서 맞춘 T 는 약 3.797 이었습니다. 보정 뒤 0.800.90 구간은 확신도 0.8555 / 정확도 0.4711 에서 확신도 0.8507 / 정확도 0.7988 로 둘이 가까워졌습니다.

▶ 써 볼 것

고객 문의 분류, 에이전트의 도구 라우팅처럼 답이 몇 개 중 하나인 문제는 생성이 필요 없습니다. 한 번의 순전파라 지연이 짧고, 출력 형식이 깨질 일이 없고, 무엇보다 믿을 만한 확률이 나옵니다. "확률 0.6 미만이면 사람에게 넘긴다" 같은 규칙은 보정된 확률 위에서만 의미가 있습니다.

■ 흔한 오해와 실수

"RFC 5011 자동 갱신을 켜 뒀으니 루트 키 교체는 신경 안 써도 된다." 자동 갱신은 리졸버가 앵커 파일에 쓸 수 있을 때만 동작합니다. 컨테이너에서 앵커를 읽기 전용으로 마운트했거나, 재시작할 때마다 이미지에 든 오래된 파일로 되돌아가면 갱신 결과가 사라집니다. ICANN 이 "자동이라고 가정하지 말고 수동으로 확인하라"고 굳이 적은 이유입니다.

"as 는 런타임에 아무 일도 하지 않는다." 지금의 tsc, Node, Bun 에서는 맞습니다. Bun --aot 에서는 틀립니다. 같은 소스가 실행 방식에 따라 "51" 과 6 을 냅니다. 단언으로 타입 오류를 덮어 온 코드일수록 AOT 로 옮길 때 행동이 바뀝니다.

"UB 는 그 줄에서만 문제를 일으킨다." 4절의 예처럼 컴파일러는 뒤에 올 UB 를 근거로 앞선 코드를 바꿀 수 있습니다. C23 이 관찰 가능한 동작에 대해서는 이를 막았지만, 관찰 가능하지 않은 저장이나 조건 검사는 여전히 지워질 수 있습니다.

"LLM 이 0.99 라고 하면 99% 맞는다." 6절의 측정에서 0.9 이상 구간의 실제 정확도는 약 70% 였습니다. 소프트맥스 출력은 보정 전에는 확률처럼 생긴 점수일 뿐입니다. 임계값 규칙을 만들기 전에 검증 세트로 구간별 정확도를 그려 봐야 합니다.

■ 참고 자료

- Rolling the Root Key — APNIC Blog (Geoff Huston)(https://blog.apnic.net/2026/10/09/rolling-the-root-key-2/)
- Preparing for the Root Zone KSK Rollover: What You Need to Know — ICANN(https://www.icann.org/en/blogs/details/preparing-for-the-root-zone-ksk-rollover-what-you-need-to-know-27-07-2026-en)
- Bun, TypeScript 를 네이티브 코드로 선행 컴파일하는 AOT 컴파일러 실험 공개 — GeekNews(https://news.hada.io/topic?id=35124)
- Bun 공식 발표 (X)(https://x.com/bunjavascript/status/2108441841498804715)
- The tilde in your PATH may not be your HOME — disconnect3d(https://disconnect3d.pl//2026/10/02/dont-put-tilde-in-your-path/)
- C 언어의 정의되지 않은 동작 줄이기 — GeekNews(https://news.hada.io/topic?id=35131)
- C 의 정의되지 않은 동작 줄이기, Kernel Recipes 2026 발표 정리 — LWN.net(https://lwn.net/Articles/1095811/)
- Valen's Memory Safety: A New Kind of Borrow Checking — verdagon.dev(https://verdagon.dev/blog/valen-group-borrowing)
- Build your own decision model — Nish Tahir(https://nishtahir.com/build-your-own-decision-model/)
- Qwen 으로 나만의 의사결정 모델 만들기 — GeekNews(https://news.hada.io/topic?id=35145)

반응형