Lowent 매뉴얼←↑→

16 권한 — 건네받는 힘

먼저 알아야 할 것

2장 첫 프로그램 · 출력하려면 input out cap io . 를 받는다
15장 효과 · 효과는 닫힌 원자의 집합이고 호출을 따라 번진다

돌아보기

15장에서 효과 줄은 op 이 무엇을 하는지를 적는다고 했다. 그렇다면 effects io 만 보고 알 수 없는 것은 무엇인가?

답. 무엇과 입출력을 하는지다. 표준출력에 쓰는지, 파일을 여는지, 연결을 여는지는 io 한 낱말로 갈리지 않는다. 이 장의 권한이 그것을 좁혀 준다 — input fs cap file_system . 을 받은 op 은 파일은 열 수 있어도 연결은 열 수 없다.

이 장의 필요성과 맥락

대부분의 언어에서 어떤 함수든 표준출력에 쓰고, 파일을 열고, 시계를 읽을 수 있다. 그 힘이 전역에 흩어져 있어서 “이 라이브러리가 네트워크에 닿는가” 를 알려면 소스 전체를 읽어야 한다. Lowent 에는 그런 주변 권한(ambient authority)이 없다. 힘은 값처럼 인자로 건네받는다. 효과(15장)와 권한은 같은 일을 두 쪽에서 적어 서로를 검사하는 짝이고, 이 장으로 제4부의 세 기둥(계약·효과·권한)이 선다.

이 장이 끝나면

권한의 종류와 각 종류가 여는 일을 알게 된다. 권한이 호출 사슬을 따라 이름으로 넘어가고, 시작점만 보고도 프로그램이 닿을 수 있는 바깥을 안다는 것을 익힌다. 효과와 권한의 짝표, 권한 없이 효과를 선언하거나 다른 종류를 건네거나 권한을 적지 않고 쓸 때의 진단을 보게 된다. 시작점이 받을 수 있는 권한의 목록과, 권한이 실행 중 비용이 없다는 것도 이해하게 된다.

이 장에서 답할 질문

  1. 권한을 인자로 넘기면 실행할 때 인자가 하나씩 늘어 느려지지 않는가?

16.1 권한의 종류#

권한은 input <이름> cap <종류> . 로 받는다. 종류가 그 권한으로 할 수 있는 일을 정한다.

권한여는 일
cap io표준 입출력
cap file_system파일과 디렉터리
cap net연결을 만들고 주고받는다
cap tty터미널의 크기와 모드
cap clock시각을 묻는다
cap random운영체제의 엔트로피
cap args · cap env프로그램 인자 · 환경 변수
cap allocator고정 창에서 메모리를 얻는다(자라지 않는다)
cap heap자라는 뿌리에서 메모리를 얻는다(운영체제가 있는 기계만)
cap atomic원자적 연산
cap mmio장치 레지스터
cap cC 함수를 부른다
cap machine기계 명령을 직접 낸다

표 16.1 — 처리기가 뜻을 정해 둔 권한의 종류

종류를 나누는 까닭은 최소 권한이다. 권한이 하나뿐이면 파일을 읽으려고 받은 자격이 연결도 열 수 있게 되고, 그러면 op 의 머리가 “이 op 이 무엇을 할 수 있는가” 를 더는 말해 주지 못한다.

16.2 권한은 사슬을 타고 내려간다#

권한을 다시 넘길 때 특별한 표기는 없다. 받은 이름을 다른 인자처럼 적는다.

examples/ch16/chain.low

module chain .
rem run: main

proc say input k cap io . input msg slice u8 . output u64 . effects io .
do
  return write_out k 1 msg .
end

proc say_twice input k cap io . input msg slice u8 . output u64 . effects io .
do
  let a u64 be say k msg .
  let b u64 be say k msg .
  return add a b .
end

proc main input k cap io . output u8 . effects io .
do
  let n u64 be say_twice k "hi\n" .
  guard eq n 6 . else return 1 .
  return 0 .
end

실행 결과

$ lowentc --run main chain.low
hi
hi
main() = 0

사슬은 이렇게 생긴다 — 바깥 → main 의 k → say_twice 의 k → say 의 k → write_out. 효과와 함께 그리면 두 화살표가 반대로 흐른다.

                  권한 (허락)                      효과 (한 일)
                  위에서 아래로 건넨다              아래에서 위로 번진다
  바깥(운영체제)
      │ cap io
      ▼
  main       k ─────────────┐                  ▲ effects io
      │                     │                  │
      ▼                     ▼                  │
  say_twice  k ─────────────┐                  ▲ effects io
      │                     │                  │
      ▼                     ▼                  │
  say        k ─────▶ write_out k 1 msg        ▲ effects io   ← 여기서 io 가 생긴다

권한은 위에서 건네받아야 쓸 수 있고, 효과는 아래에서 생겨 부르는 쪽 머리마다 적힌다. 그래서 어느 op 의 머리만 보면 «이 op 이 무엇을 할 수 있고(효과), 누가 그것을 허락했는지(권한)» 가 다 보인다. 권한을 받지 않은 op 은 say 를 부를 수 없다. 넘길 것이 없기 때문이다. 그래서 “이 프로그램이 파일을 건드릴 수 있는가” 를 시작점만 보고 답할 수 있다. 깊은 곳의 어떤 op 이 몰래 파일을 여는 일은 있을 수 없다. 권한을 받지 않았다면 열 수 없고, 받았다면 그 사슬이 시작점까지 이어져 보인다.

문. 권한을 인자로 넘기면 실행할 때 인자가 하나씩 늘어 느려지지 않는가?

답. 늘지 않는다. 권한은 번역 시점의 표시이고 실행 중의 값이 아니다. 권한을 받는 op 과 받지 않는 op 은 C 경계에서 같은 서명을 갖는다. 권한은 “이 op 을 부를 자격이 있는가” 를 번역할 때 묻고, 실행할 때는 물을 것이 남지 않는다. 경계를 지키는 값이 실행 비용으로 돌아오지 않는다.

16.3 효과와 권한은 짝이다#

효과 줄은 op 이 무엇을 하는지, 권한 입력은 누가 허락했는지를 적는다. 같은 일을 두 쪽에서 적고 서로를 검사한다.

효과허락하는 권한없을 때
iocap io · file_system · net · tty · clock · randomE-EFFECT-NO-CAP
alloccap allocator(또는 영역)E-ALLOC-NOCAP
heapcap heapE-HEAP-NOCAP
atomiccap atomicE-ATOMIC-NOCAP
devicecap mmioE-MMIO-NOCAP
state · panic · wait · concurrent권한 없이 적는다—

표 16.2 — 효과와 그것을 허락하는 권한

examples/ch16/pairs.low

module pairs .
rem run: main

proc main input out cap io . input al cap allocator . output u8 . effects io alloc .
do
  let g option mut slice u8 be alloc_bytes al capacity 16 .
  guard is_some g . else return 1 .
  let n u64 be write_out out 1 "hi\n" .
  return 0 .
end

실행 결과

$ lowentc --run main pairs.low
hi
main() = 0

main 이 두 권한을 받는다. cap io 가 effects io 를, cap allocator 가 effects alloc 을 허락한다. alloc_bytes al capacity 16 은 할당기에서 16 바이트를 청하고, 얻지 못할 수 있으므로 option 을 준다.

짝이 어긋나는 모양은 셋이다. 효과를 적었는데 허락하는 권한이 없으면 거절된다.

examples/ch16/nocap.low

module nocap .
rem expect: E-EFFECT-NO-CAP

proc main output u8 . effects io .
do
  let n u64 be write_out 1 "hello\n" .
  return narrow u8 n .
end

실행 결과

$ lowentc --check nocap.low
nocap.low:4:0 E-EFFECT-NO-CAP: this op declares the `io` effect but receives NO capability that authorizes it. `io` is a cap-effect (RFC-0007 §6.7): I/O is a RIGHT you are HANDED, not an ambient power — `input fs cap file_system .` (or another `cap …` input). An effect you declare but hold no capability for is a claim the signature cannot back

할당도 같다. 메모리를 쓰겠다고 적고 그 자리를 어디서 받는지 적지 않았다.

examples/ch16/alloc_nocap.low

module alloc_nocap .
rem expect: E-ALLOC-NOCAP

proc scratch output u8 . effects alloc .
do
  return 1 .
end

실행 결과

$ lowentc --check alloc_nocap.low
alloc_nocap.low:4:0 E-ALLOC-NOCAP: this op declares the `alloc` effect but receives NO allocation capability. There is no ambient heap in this language (RFC-0043 D1): to allocate you must be HANDED the right — `input a cap allocator .` or `input r region <name> . .` (a region IS an arena allocator, RFC-0043 D7). An allocation nobody granted is exactly the hidden dependency this model removes
alloc_nocap.low:5:1 W-EFFECT-OVER: this op DECLARES `alloc` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

권한을 받아 두기만 하고 쓰는 자리에 건네지 않아도 거절된다. 권한을 요구하는 내장 op 은 그 권한을 첫 피연산자로 적는다.

examples/ch16/missing.low

module missing .
rem expect: E-CAP-MISSING

proc hello input out cap io . output u64 . effects io .
do
  return write_out 1 "hello\n" .
end

실행 결과

$ lowentc --check missing.low
missing.low:6:0 E-CAP-MISSING: `write_out` writes to the process output and needs the `cap io` value the entry received as its FIRST operand — output is a RIGHT you are handed (RFC-0030 D2), never ambient. There is no hidden stdout

이름 없이 권한에 닿는 길은 없다. 숨은 표준출력이 없다는 뜻이다.

16.4 있음이 아니라 종류다#

권한 값을 하나 가지고 있다고 모든 문이 열리지는 않는다.

examples/ch16/kind.low

module kind .
rem expect: E-CAP-KIND

proc stamp input out cap io . output u64 . effects none .
do
  return time_now out .
end

실행 결과

$ lowentc --check kind.low
kind.low:6:0 E-CAP-KIND: this host leaf was handed a capability of the WRONG KIND. Authority is by KIND, not by mere presence: an unrelated right (io, net, …) does not authorize the clock, the filesystem, the socket or the OS entropy (RFC-0083 L3 · RFC-0077 §P1-2). Take the right this leaf names — the FFI boundary has always been checked this way (E-FFI-CAPKIND); the host leaves used to be refused only at lowering, where a WRONG RIGHT was reported as an UNSUPPORTED FEATURE

time_now 는 시계를 읽는 원시 연산이다. cap io 를 건넸으므로 종류가 틀렸다고 거절된다. 권한은 무엇인가로 인가된다.

흔한 오해. cap clock 으로 시각을 읽으면 io 효과를 적어야 한다

표준 라이브러리의 clock.now_ms 는 cap clock 을 받지만 effects none 이다. 시계는 결정성을 깨므로 권한이 필요하지만, 바깥에 흔적을 남기지는 않는다. 권한과 효과가 늘 일대일은 아니다. 짝표의 io 줄은 “io 효과를 내려면 이 가운데 하나를 받아야 한다” 는 방향의 규칙이다.

권한의 이름은 저자가 지을 수 있다. input logger cap audit . 처럼 자기 권한을 만들어 매개변수로 받으면, 그 권한을 받은 op 만 부를 수 있는 op 을 만들 수 있다. 처리기가 뜻을 정해 둔 것은 위 표의 종류들이고, 그 종류의 일을 하려면 그 종류를 건네야 한다.

16.5 시작점이 받는 것#

프로그램이 시작하는 op 은 권한만 받는다. 자료를 받지 않는다. 건네줄 사람이 없기 때문이다.

examples/ch16/entry.low

module entry .
rem expect: E-ENTRY-PARAMS

proc main input out cap io . input n u64 . output u8 . effects io .
do
  return narrow u8 n .
end

실행 결과

$ lowentc --check entry.low
entry.low:4:0 E-ENTRY-PARAMS: the entry point takes CAPABILITY inputs only (`input a cap args .`). A data parameter here would be a magic argv — RFC-0030 D2 rejects that: arguments are a RIGHT the runtime hands you, queried via `count`/`arg` on the cap
entry.low:5:1 W-EFFECT-OVER: this op DECLARES `io` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

C 의 argv 같은 마법의 인자는 없다. 프로그램 인자도 cap args 로 받는 권한이다. 시작점이 받을 수 있는 권한은 닫힌 목록이다 — args·env·io·allocator·heap·file_system·net·tty·clock·random·atomic. 그 밖의 권한(mmio·machine·c)은 실행하는 쪽이 건넬 수 있는 것이 아니라서 시작점이 요구하면 E-ENTRY-CAP 으로 거절된다. 그런 권한은 그것을 줄 자격이 있는 자리에서 만들어져 인자로 흘러야 한다(29·30장).

실제 사례. 의존성이 네트워크에 닿는지 알고 싶을 때

남이 만든 라이브러리를 들여올 때 가장 불안한 것은 그 코드가 무엇에 닿는지다. Lowent 에서는 그 라이브러리의 export 된 op 머리를 훑어 cap net 을 받는 op 이 있는지 보면 된다(C 로 빠져나가는 cap c 와 기계 명령의 cap machine 도 함께 본다 — 그 안은 처리기가 보지 못한다). 없다면 그 라이브러리는 연결을 열 수 없다. 있다면 그 op 을 부르는 자리에 내가 cap net 을 건네야 하므로, 그 자리가 곧 감사할 자리다. 공급망 공격의 흔한 모양 — 들여온 코드가 몰래 바깥에 연결한다 — 이 문법으로 드러난다.

환경 변수와 운영체제의 난수도 권한으로 받는다.

examples/ch16/envrandom.low

module envrandom .
rem run: main

use random .

rem 환경 변수는 cap env, 운영체제의 엔트로피는 cap random — 둘 다 받아야 닿는다
proc main input e cap env . input r cap random . input al cap allocator . output u8 . effects alloc .
do
  let g option mut slice u8 . . be alloc_bytes al capacity 8 .
  guard is_some g . else return 250 .
  var buf mut slice u8 . be some_value g .
  rem 여덟 바이트를 운영체제의 난수로 채운다 — 값은 매번 다르므로 돌려주지 않는다
  let n u64 be random.bytes r buf .
  rem 환경 변수가 없으면 none — 값이 아니라 없음을 돌려준다
  let v option slice u8 be env_get e "LOWENT_MANUAL_DEMO" .
  guard is_some v . else return narrow u8 n .
  return 99 .
end

실행 결과

$ lowentc --run main envrandom.low
main() = 8

env_get e "…" 은 cap env 가, random.bytes r buf 는 cap random 이 있어야 부를 수 있다. 환경 변수는 프로그램 밖에서 조용히 동작을 바꾸는 통로이고, 난수는 같은 입력에도 답을 바꾸는 통로다. 둘 다 머리에 보여야 “이 프로그램의 답은 입력만으로 정해지는가” 를 머리만 보고 판단할 수 있다. 변수가 없으면 env_get 은 none 이고, 예제는 채운 바이트 수 8 을 돌려준다. 시험에서 되풀이할 수 있는 난수가 필요하면 권한이 필요 없는 rng_next (씨앗에서 다음 수)를 쓴다.

16.6 흔한 실수#

반례. 권한을 받았으니 fn 도 출력할 수 있다고 생각한다

examples/ch16/mistake_fncap.low

module mistake_fncap .
rem expect: E-EFFECT-CALC

rem ✘ 권한을 받았다고 `fn` 이 출력할 수 있게 되지는 않는다
fn say input out cap io . output u64 .
do
  return write_out out 1 "hi\n" .
end

실행 결과

$ lowentc --check mistake_fncap.low
mistake_fncap.low:6:1 E-EFFECT-CALC: this fn is declared pure but performs `io` — make it a `proc` with `effects …`, or remove the effect

권한은 “누가 허락했는가” 이고 효과는 “무엇을 하는가” 다. 둘은 서로를 대신하지 않는다. cap io 를 받았어도 출력은 io 효과이고, 순수한 fn 은 효과를 낼 수 없으므로 E-EFFECT-CALC 다. proc say … effects io . 로 바꾼다. 허락(권한)과 행동(효과)을 모두 머리에 적어야 두 쪽이 서로를 검사한다.

반례. 받은 권한을 지역 이름에 옮겨 담는다

examples/ch16/mistake_caplocal.low

module mistake_caplocal .
rem expect: E-CAP-LOCAL

proc say input out cap io . output u64 . effects io .
do
  rem ✘ 권한을 지역 이름에 옮겨 담았다 --- 받은 이름을 그대로 건넨다
  let k cap io be out .
  return write_out k 1 "hi\n" .
end

실행 결과

$ lowentc --check mistake_caplocal.low
mistake_caplocal.low:8:0 E-CAP-LOCAL: `k` is a LOCAL that a `cap io` was copied into. A capability is not a value you carry in a local name: only a parameter of this op names a right it was handed, so hand the parameter itself to the leaf (RFC-0030 D2)

권한은 사슬을 따라 받은 이름 그대로 넘긴다. 지역 이름에 옮겨 담으면 권한이 어디서 왔는지 사슬이 끊겨 보이므로 E-CAP-LOCAL 로 거절한다. 진단은 옮겨 담은 지역 이름과 그 종류를 짚어 원인을 말한다. write_out out 1 … 처럼 매개변수 이름을 곧바로 적는다.

반례. 다른 종류의 권한으로 표준출력에 쓴다

examples/ch16/mistake_fscapout.low

module mistake_fscapout .
rem expect: E-CAP-KIND

rem ✘ 파일 권한으로 표준출력에 쓰려 한다
proc main input fs cap file_system . output u8 . effects io .
do
  let n u64 be write_out fs 1 "hi\n" .
  return 0 .
end

실행 결과

$ lowentc --check mistake_fscapout.low
mistake_fscapout.low:7:0 E-CAP-KIND: this leaf needs a `cap io`, and `fs` is a `cap file_system`. One capability never stands in for another: each names a different right, and holding one says nothing about the other (RFC-0011)

cap file_system 은 파일과 디렉터리를 여는 권한이지 표준출력의 권한이 아니다. 권한은 있음이 아니라 종류로 인가되므로 E-CAP-KIND 로 거절된다 — 진단이 필요한 종류와 건넨 종류를 나란히 적는다. 고치는 길은 시작점이 input out cap io . 를 받아 그 이름을 건네는 것이다.

반례. 권한 자리에 수를 넘긴다

examples/ch16/mistake_capforge.low

module mistake_capforge .
rem expect: E-CAP-FORGE

proc say input k cap io . output u64 . effects io .
do
  return write_out k 1 "hi\n" .
end

rem ✘ 권한을 하나도 받지 않았는데, 권한 자리에 수 0 을 넘겨 출력한다
proc start output u8 .
do
  let n u64 be say 0 .
  return 0 .
end

실행 결과

$ lowentc --check mistake_capforge.low
mistake_capforge.low:12:0 E-CAP-FORGE: a LITERAL was passed where a capability is taken. A capability is handed over, never conjured: it can only be a name you were given (an `input … cap …`) or an actor's capability field (RFC-0030 D2). Passing a number here would let an op that received NO right reach the outside, and then the entry point no longer tells what the program can touch. The placeholder `0` means something only at the tool's door (`--run`)

start 는 권한을 하나도 받지 않았다. 그러니 출력할 수 없어야 한다. say 의 cap io 자리에 수 0 을 넘기는 것이 그 규칙을 피해 가는 길처럼 보이지만, E-CAP-FORGE 로 거절된다. 권한 자리에 올 수 있는 것은 건네받은 이름(input … cap …)이나 액터의 권한 칸뿐이다. 이것이 막히지 않으면 “시작점만 보면 닿을 수 있는 바깥을 안다” 는 이 장의 약속이 한 줄로 무너진다. --run 이 쓰는 자리표 0 은 도구의 입구에서만 뜻이 있다(31장).

흔한 오해. 나중에 쓸지도 모르니 권한과 효과를 미리 받아 두면 편하다

examples/ch16/capjustincase.low

module capjustincase .
rem expect: W-EFFECT-OVER

rem 나중에 쓸지도 몰라 권한과 효과를 미리 적어 두었다
proc area input out cap io . input w u64 . input h u64 . output u64 . effects io .
  requires le w 1000 .
  requires le h 1000 .
do
  return mul w h .
end

실행 결과

$ lowentc --check capjustincase.low
capjustincase.low:8:1 W-EFFECT-OVER: this op DECLARES `io` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

area 는 곱셈만 하는데 cap io 와 effects io 를 적었다. 그러면 이 op 을 부르는 모든 쪽이 cap io 를 구해 건네야 하고, 순수한 계산에서는 부를 수조차 없다. 도구는 W-EFFECT-OVER 로 알린다. 권한은 최소한으로 받는다 — 받은 권한의 목록이 곧 “이 op 이 닿을 수 있는 바깥” 이라서, 넉넉하게 받으면 그 목록이 거짓말을 한다.

16.7 이 장의 문법 한눈에#

모양뜻왜 이렇게
input out cap io .표준 입출력의 권한을 받는다주변 권한이 없다 — 힘은 인자로 건네받는다
input fs cap file_system . · cap net · cap clock …종류마다 여는 일이 다르다최소 권한 — 머리가 닿을 수 있는 바깥을 말한다
say k msg받은 권한 이름을 그대로 넘긴다사슬이 시작점까지 이어져 보인다
write_out out 1 "…"권한을 쓰는 내장 op 은 권한을 첫 피연산자로이름 없이 권한에 닿는 길이 없다
effects io + cap io효과와 그것을 허락하는 권한의 짝없으면 E-EFFECT-NO-CAP
effects alloc + cap allocator고정 창 할당의 짝없으면 E-ALLOC-NOCAP
proc main input … cap … . output u8 .시작점은 권한만 받는다건네줄 사람이 없으니 자료는 받지 않는다
input logger cap audit .저자가 이름 지은 권한그 권한을 받은 op 만 부를 수 있게 한다
env_get e "…" · random.bytes r buf환경 변수 · 운영체제 난수 — cap env · cap random답을 바꾸는 바깥 통로가 머리에 보인다

표 16.3 — 권한의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나

복습 정리

권한은 input <이름> cap <종류> . 로 건네받고, 받은 이름을 그대로 넘긴다. 시작점만 보면 프로그램이 닿을 수 있는 바깥을 안다. 효과와 권한은 짝이라 효과를 적으면 허락하는 권한을 받아야 하고, 권한을 쓰는 내장 op 은 권한을 첫 피연산자로 적는다. 권한은 종류로 인가되며 실행 비용이 없다. 시작점은 닫힌 목록의 권한만 받는다.