move: capability가 struct 필드로 복제되던 구멍을 막는다 (보안 정리 ii)

"발견들 다 고쳤나"를 확인하려고 전부 다시 돌려보다 드러났다.

  pub struct Wrapper { pay: Pay }
  pub fn duplicate(w: Wrapper) effects {Pay.charge} {
      consume(w.pay)
      consume(w.pay)      // 같은 capability를 두 번 소비 — 통과했다
  }

w가 빌린 값인데도 통과했다. 즉 "safe code에서 capability는 복제·위조되지
않는다"가 깨져 있었다. 이 세션에서 찾은 것 중 가장 심각하다.

원인은 단순하다. E_field가 빌린 값을 돌려주는데 Move 문맥에서 그것을
검사하는 곳이 E_ident 분기에만 있었고, 필드 접근은 그 분기를 지나가지
않는다. 규칙("v0에 부분 move는 없다")은 주석에 적혀 있었으나 강제되지
않았다.

필드의 affinity를 알아야 정확히 막을 수 있어서 move 검사기에 struct 필드
표와 바인딩의 선언 타입을 넣었다. copyable 필드는 막지 않는다 — w.label은
통과한다.

이것이 LRU에서 열거형으로 우회한 것을 사후에 정당화한다. 그때는 "struct로는
안 되고 열거형으로는 된다"가 우연처럼 보였는데, 열거형이 유일한 길인 것이
규칙이었고 struct 쪽이 새고 있었을 뿐이다.

여태 안 보인 이유: samples/05는 자원 타입을 직접 다루고 struct에 담지
않는다. 개밥 먹기에서 자원을 자료구조에 담는 코드를 처음 쓰면서 드러났다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
2026-08-30 19:21:16 +09:00
co-authored by Claude Opus 5
parent 450e96d326
commit 01d090b065
3 changed files with 130 additions and 12 deletions
+30
View File
@@ -1732,3 +1732,33 @@ let () =
\ }\n\
}"
"match 팔의 타입이 서로 다릅니다")
(* 보안 정리 (ii): safe code에서 capability는 복제되지 않는다.
struct 필드를 통한 구멍이 있었다 — 필드 접근이 빌린 값을 돌려주는데
Move 자리에서 그것을 검사하는 곳이 없었다. 빌린 wrapper에서도 됐다. *)
let () =
let src =
"capability Pay {\n\
\ fn charge(n: Int) effects {Pay.charge}\n\
}\n\n\
struct Wrapper {\n\
\ pay: Pay,\n\
\ label: String,\n\
}\n\n\
fn consume(own p: Pay) effects {Pay.charge}\n\n"
in
check "capability를 필드로 복제할 수 없다"
(move_has
(src
^ "fn duplicate(w: Wrapper) effects {Pay.charge} {\n\
\ consume(w.pay)\n\
\ consume(w.pay)\n\
}")
"부분 move가 없습니다");
check "소유한 struct에서도 필드를 꺼낼 수 없다"
(move_has
(src
^ "fn take(own w: Wrapper) effects {Pay.charge} {\n consume(w.pay)\n}")
"부분 move가 없습니다");
check "copyable 필드는 막지 않는다"
(move_errs (src ^ "fn label_of(w: Wrapper) -> String {\n w.label\n}") = [])