OpenAI Codex · Code Mode

Linux: 먼저 뷰를 만들고, 그다음 seccomp, 마지막에 exec

같은 읽기인데 Mac은 Operation not permitted, Linux는 No such file or directory를 돌려줘요. 차이는 Linux가 먼저 프로세스가 보는 파일 트리를 바꾸고, 그다음 호출할 수 있는 시스템 인터페이스를 조인다는 점이에요.

강의 목표읽고 나면 세 가지를 말할 수 있어요. Linux 기본 경로가 왜 두 박자인지: 먼저 bubblewrap으로 파일시스템 뷰를 만들고 그다음 seccomp. 왜 helper를 따로 띄우는지. 타입 이름은 왜 여전히 LandlockCommand인데 기본은 bubblewrap인지.
먼저 한 번 써 보세요 · 요청 하나가 두 층을 지나가요
프로브를 바꾸고 스위치를 돌려, 요청이 어느 층에서 멈추는지 보세요
프로브
스위치
기본 경로에서 /etc/shadow 읽기가 뷰에서 멈추는지 먼저 보세요. connect로 바꾸면 같은 길이 필터까지 가요.
이번 요청
open /etc/shadow
현재 경로
기본 두 단계
1층 · 사전 검사대기
transform은 먼저 helper가 있는지, WSL1인지, user namespace를 만들 수 있는지 물어요. 못 들어가면 여기서 멈춰요.
2층 · 뷰대기
bubblewrap이 먼저 파일 트리를 바꿔요. 기본은 읽기 전용이고 쓰기 루트를 겹칩니다. 마운트에 없는 경로는 뒤에 파일이 없다고 나와요.
3층 · seccomp대기
뷰가 만들어진 뒤에야 no_new_privs를 켜고 네트워크 필터를 설치해요. 규칙에 걸리면 EPERM이에요.
재생을 눌러, 이 요청이 두 층을 차례로 지나는 모습을 보세요.
논리 궤적 · 애니메이션 각 단계가 소스 어디에 대응하는지
  1. 권한 프로필을 읽고 WSL1 사전 검사를 해요manager.rs L413
  2. helper argv를 만들고 권한 프로필 JSON을 붙여요landlock.rs L23
  3. 바깥 run_main이 bwrap인지 지름길인지 정해요linux_run_main.rs L152
  4. 안쪽 명령을 만들고 --apply-seccomp-then-exec를 붙여요linux_run_main.rs L1520
  5. bwrap이 순서대로 ro-bind, bind, unshare를 해요bwrap.rs L306
  6. bwrap을 exec할 때 --as-pid-1을 넣어요launcher.rs L39
  7. 안쪽에서 capget으로 능력이 0인지 확인해요linux_run_main.rs L216
  8. PR_SET_NO_NEW_PRIVS를 켠 다음 seccomp를 설치해요landlock.rs L61
  9. 사용자 명령을 fork하고 부모는 waitpid로 고아를 거둬요linux_run_main.rs L243
  10. execvp가 프로세스 이미지를 이어받아요linux_run_main.rs L1565
재생을 눌러, 이 요청이 두 층을 차례로 지나는 모습을 보세요.
뷰가 막는 것은 보는 일이에요경로가 마운트에 없으면 커널이 No such file or directory를 돌려줘요. 이 층은 아직 시스템 호출 표 차례가 아니에요.
seccomp가 막는 것은 호출이에요필터는 기본이 허용이고, connect, bind, ptrace 같은 규칙에 걸려야 EPERM이에요. 이미 안 보이는 파일은 맡지 않아요.
못 들어가면 분명히 말해요WSL1이거나 user namespace가 없으면 명령은 transform에서 멈춰요. 기본 경로 실패는 몰래 Landlock으로 바꾸지 않아요.
수업용 스케치: 경로와 프로브는 수업용 설정이에요. 논리 궤적 오른쪽 행 번호는 openai/codex 저장소 commit 4f39251a01에 대응해요.
아이디어 1 · 먼저 볼 수 있는 세계를 바꾸고, 그다음 호출을 조여요
어떤 문제를 푸는가

동료가 Mac에서 Codex를 돌리면 에이전트가 ~/.ssh/id_rsa를 읽고 Seatbelt가 바로 Operation not permitted를 돌려줘요. 같은 저장소를 Linux 노트북에 두면 흔한 문구가 No such file or directory가 돼요. 경로는 종종 마운트에 아예 없어요.

PR_SET_NO_NEW_PRIVS를 먼저 켠 다음 배포판 기본 bwrap을 부르면, 많은 배포판의 setuid 바이너리가 아예 안 떠요. 가장 많이 설치된 기계에서 샌드박스가 오히려 실패해요. seccomp는 프로세스가 아직 호출할 수 있는 시스템 인터페이스를 정하는 커널 필터이고, no_new_privs는 그 전제 조건이자 setuid 권한 상승도 막아요.

아이디어는 무엇인가

기본 경로는 같은 helper에 두 번 들어가요. 바깥은 bubblewrap만 짜서 파일시스템을 기본 읽기 전용으로 바꾸고 쓰기 루트를 겹칩니다. .git, .agents, .codex는 쓰기 루트 안에 있어도 읽기 전용이에요. 안쪽에서야 no_new_privs를 켜고 네트워크 seccomp를 설치한 뒤 fork해서 프로세스 이미지를 사용자 명령에 넘겨요.

함수 헤더는 순서를 세 단계로 적어요.

codex-rs/linux-sandbox/src/linux_run_main.rs152–158행
/// Entry point for the Linux sandbox helper.
///
/// The sequence is:
/// 1. When needed, wrap the command with bubblewrap to construct the
///    filesystem view.
/// 2. Apply in-process restrictions (no_new_privs + seccomp).
/// 3. `execvp` into the final command.
소스 스냅샷 설명: 로컬 저장소 openai/codex 기준, 확인 파일 codex-rs/linux-sandbox/src/linux_run_main.rs, commit 4f39251a01, 확인일 2026-08-22. 코드 블록은 소스 원문을 유지하고, 이 세 단계가 기본 경로의 계약이에요.

필드 주석이 이유를 깃발에 적어요. bubblewrap은 setuid에 의존할 수 있으니 뷰를 먼저 만들고 그다음 조여야 해요.출처: codex-rs/linux-sandbox/src/linux_run_main.rs 117–121행;codex-rs/linux-sandbox/src/landlock.rs 57–65행

바깥 helper bubblewrap 깃발을 짜요 --ro-bind 다음에 --bind 먼저 파일 트리를 바꿔요 아직 no_new_privs는 건드리지 않아요 안쪽 helper capget으로 능력이 0인지 확인 no_new_privs 더하기 seccomp 그다음 시스템 호출을 조여요 --apply-seccomp-then-exec를 붙여요 execvp 사용자 명령 프로세스를 이어받아요
수업용 구조도: 같은 바이너리가 두 번 들어가고, 가운데는 bubblewrap이 만든 세계예요.
왜 오래가는가

무엇을 볼 수 있는지와 어떤 syscall을 호출할 수 있는지는 벽 두 개예요. 세계를 먼저 바꾸고 호출을 조여요. 다른 언어로 다시 써도 배포판에 setuid 래퍼가 남아 있으면 이 순서는 그대로예요.

아이디어 2 · helper를 따로 띄워 고아를 거둘 사람을 남겨 둬요
어떤 문제를 푸는가

bubblewrap과 seccomp는 곧 사용자 명령이 될 그 프로세스에서 일어나야 해요. 메인 CLI가 스스로 unshare한 뒤 exec하면 실패가 세션 전체를 가져가고, PID namespace에 고아를 거둘 1번 프로세스를 남길 수도 없어요. 타임아웃과 취소는 잠든 프로세스를 남겨요.

아이디어는 무엇인가

Linux는 argv[0]의 basename을 봐요. 파일 이름이 이미 codex-linux-sandbox면 그대로 두고, 아니면 이 별명으로 바꿔 같은 바이너리를 helper로 들여요. 바깥 exec는 ls가 아니라 helper 자신이고 --apply-seccomp-then-exec를 달고 있어요. launcher는 --as-pid-1도 넣어 bubblewrap 자신이 네임스페이스의 1번 프로세스가 되게 해요.

안쪽은 먼저 capget이에요. effective나 permitted가 하나라도 0이 아니면 바로 panic이고 명령은 안 달려요. 이 검사를 지나야 fork해요. 자식은 execvp, 부모는 waitpid(-1)로 자손을 거두고 종료 코드를 그대로 전해요.출처: codex-rs/linux-sandbox/src/linux_run_main.rs 216–221행、242–243행;codex-rs/linux-sandbox/src/launcher.rs 38–39행

SandboxManager 바깥 helper 안쪽 argv를 짜요 bubblewrap --as-pid-1 안쪽에서 명령을 fork해요 자식 execvp 부모가 남아요 waitpid가 고아를 거둬요 취소와 타임아웃은 남아 있는 그 1번 프로세스에 기대요
수업용 시퀀스: 프로세스를 넘긴 뒤에도 거두는 사람은 남아 있어요.
왜 오래가는가

독립 보조 프로세스에 먼저 래핑하고 그다음 조이는 것은 플랫폼을 가로지르는 같은 종류의 자기 호출이에요. Windows는 숨은 인자를 보고 Linux는 arg0을 봐요. 래퍼가 실패하면 고정 종료 코드와 고정 stderr 접두사가 자동 귀속에 더 좋아요.

아이디어 3 · 기본은 Landlock이 아니고, 실패해도 후퇴하지 않아요
어떤 문제를 푸는가

타입 이름은 여전히 LandlockCommand이고 crate 디렉터리도 linux-sandbox라서, 온라인 자료 상당수가 Codex가 Linux에서 Landlock을 쓴다고 적어요. 지금 기본 파일시스템 샌드박스는 이미 bubblewrap이에요. Landlock은 명시적으로 켜는 legacy 후퇴이고, 기능 스위치는 Deprecated이며 기본은 꺼져 있어요. legacy는 제한 읽기도, 갈라낸 중첩 읽기 전용도 담지 못해요.

bwrap이 실패한 뒤 조용히 Landlock으로 바꾸면, 모델은 “뷰 안에 갇혀 있어요”라고 계획하는데 실제로는 호스트 파일 트리가 보여요. 다음 걸음이 갈라져요.

아이디어는 무엇인가

바깥은 never falls back라고 적어요. bubblewrap 실패는 실패예요. WSL1에서 bubblewrap이 필요하면 transformWsl1UnsupportedForBubblewrap을 돌려주고 명령은 helper에 들어가지 않아요. 디스크 전체가 쓰기 가능하고 프록시를 안 쓰면 bwrap에 아예 안 들어가서 WSL1도 통과할 수 있어요. user namespace를 못 만들면 시작 경고이지, 더 약한 백엔드로 바꾸지 않아요.출처: codex-rs/linux-sandbox/src/linux_run_main.rs 290–294행;codex-rs/sandboxing/src/manager.rs 413–420행、696–710행;codex-rs/features/src/lib.rs 1064–1069행

로컬 배포판 user namespace가 있어요 시스템 bwrap 또는 묶인 바이너리 기본 두 단계 CI 컨테이너 user namespace를 막았어요 조용히 Landlock으로 바꾸지 않아요 시작 경고, 하드 실패 WSL1 user namespace를 만들 수 없어요 bwrap이 필요한 명령은 못 들어가요 transform 단계에서 거절
수업용 구조도: 모두 Linux 샌드박스라고 부르지만, 기계 세 대에서 실제로 켜진 층은 같지 않아요.
보는 일과 호출은 벽 두 개예요. 못 들어가면 분명히 말하고, “이미 샌드박스됐어요”라는 한 문장으로 합성하지 마세요.
왜 오래가는가

플랫폼 능력이 다를 때 가장 쉬운 실수는 “우리에게 Linux 샌드박스가 있어요”로 모든 Linux를 덮는 거예요. 새 bwrap이 있는 노트북, user namespace를 막은 CI 컨테이너, 아직 WSL1인 동료 — 기계 세 대는 같은 벽이 아니에요. 제품 문구와 모델 설명은 실제로 켜진 그 층으로 써야 해요.

가로 비교 · 세 박자를 나눈 뒤 각자가 무엇을 새는지

DeepSeek Harness: 탐침이 통과하면 기어를 바꾸고 한 박자로 합성해요

DSH의 Linux 체인은 bwrap 다음에 landlock으로 고정돼 있어요. bwrap 단계는 뷰 더하기 exec이고 seccomp는 없어요. 탐침이 실패해야 landlock-run으로 가요. 자기 몸에 규칙을 설치한 뒤 대상 명령을 exec해요. 파일시스템 제한과 exec가 같은 main에 있어요. 이 경로에는 setuid bwrap이 없어서 필터를 다음 박자로 나눌 필요가 없어요.

양쪽 모두 독립 보조 프로세스를 써요. DSH 런처 실패는 125이고 landlock-run: 치명 줄도 같이 봐야 자동 귀속이 더 쉬워요. Codex helper 실패는 panic이거나 wait status 그대로예요. 체인 둘의 탐침이 모두 실패하면 DSH는 맨몸 실행을 거절해요. 탐침한 두 번째 단계로 바꿀 수는 있고, 바꾼 뒤에도 fail closed예요. Codex 기본 경로 실패는 Landlock으로 후퇴하지 않아요.출처: packages/sandbox/sandbox-local/src/index.ts 159–166행;native/landlock-run/packages/entry/src/main.c 254–261행

DSH PLATFORM_CHAINS와 landlock-run 확인 · 2026-08-22 · DSH · 샌드박스

Grok: 세 박자를 에이전트에 치고, 실패하면 후퇴할 수 있어요

Grok의 순서는 Codex와 같은 공학 이유예요. setuid에 의존할 수 있는 bwrap을 먼저 끝낸 뒤, 이미 네임스페이스에 들어간 프로세스에서 조여요. 차이는 주체예요. Grok은 세 박자를 에이전트 프로세스에 치고 명령은 결과만 상속해요. 뷰는 패치식 --bind / / 위에 deny 경로를 덮는 것이라 호스트 트리가 남아 있어요. 자식 spawn 때 네트워크를 제한해야 하면 pre_exec에서 seccomp를 설치해요.

bwrap exec가 실패하고 read-deny가 필요 없으면 경고 한 줄 하고 Landlock으로 후퇴해요. 내장 Profile의 apply 실패는 경고를 남기고 계속하며, is_active()가 진짜 상태예요. Codex 바깥은 never falls back라고 적어요.출처: crates/codegen/xai-grok-shell/src/config/mod.rs 1293–1320행;crates/codegen/xai-grok-sandbox/src/lib.rs 181–192행

Grok bwrap_reexec_for_profile과 apply 확인 · 2026-08-22 · Grok · 샌드박스 Profile 다섯 가지
수업 실습
01

같은 읽기, 스위치를 바꾼 뒤 어디에 걸리는지

프로브는 /etc/shadow 읽기에 두고 WSL1을 먼저 켜세요. 예측을 적어요. 어느 층에서 멈추고 문구가 무엇인지. 그다음 WSL1을 끄고 legacy Landlock을 켜서 제한 읽기가 UnsupportedOperation이 되는지 보세요.

심화 질문: 프로브를 외부 connect로 바꾸세요. 기본 경로가 왜 뷰를 지나 seccomp에서 멈추고, 문구가 No such file에서 EPERM으로 바뀌는지.

Takeaway: Linux 기본 경로는 세 박자예요. 먼저 bubblewrap으로 파일시스템 뷰를 바꾸고, 이미 네임스페이스에 들어간 helper에서 seccomp를 올린 뒤에야 exec해요. 순서를 뒤집으면 setuid 시스템 bwrap이 안 떠요. 타입 이름은 여전히 Landlock — 호환이지 현재 기본이 아니에요. 못 들어가면 spawn 전에 거절하고, 실패해도 더 약한 백엔드로 바꾸지 않아요.