Compare commits
450 Commits
90b86a5531
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
07194c74cb
|
|||
|
4cf694d2b3
|
|||
|
c9facb746b
|
|||
|
878b66022e
|
|||
|
2e0a4795f6
|
|||
|
c328b584c0
|
|||
|
9585b35d5b
|
|||
|
26cafe3e80
|
|||
|
125f150784
|
|||
|
0dcac55a0c
|
|||
|
6d202d73b4
|
|||
|
1438096339
|
|||
|
059164d4fa
|
|||
|
8db906ee64
|
|||
|
cedfceded5
|
|||
|
33d2dcce1b
|
|||
|
2baa2d7063
|
|||
|
0166833431
|
|||
|
b3da3da525
|
|||
|
1b3902df78
|
|||
|
ea1e3ebae9
|
|||
|
1c692bfb79
|
|||
|
141a18999f
|
|||
|
afe23600d2
|
|||
|
09d2844981
|
|||
|
d500d6e559
|
|||
|
5b73316ae0
|
|||
|
5d8a2199b6
|
|||
|
a1482ecdd0
|
|||
|
a07f9ed84c
|
|||
|
51304b03af
|
|||
|
c6397b941f
|
|||
|
d65e5f817a
|
|||
|
696e593898
|
|||
|
97ab24feef
|
|||
|
31f0dd36df
|
|||
|
9aec2f46fe
|
|||
|
022cc26b2e
|
|||
|
b4c018da8f
|
|||
|
66f52407d3
|
|||
|
e463faf649
|
|||
|
375acb476d
|
|||
|
c81c9a9d75
|
|||
|
339e4080dc
|
|||
|
e0533aaa68
|
|||
|
13c7083bc0
|
|||
|
6947ff04e0
|
|||
|
140fe21237
|
|||
|
f52d2c7db6
|
|||
|
3c9e547c4a
|
|||
|
a3988c1a77
|
|||
|
5db0714072
|
|||
|
69a4ab8105
|
|||
|
22d577ab49
|
|||
|
83a1c75f1a
|
|||
|
0ac6e99818
|
|||
|
f35733810e
|
|||
|
9c1a5d43ba
|
|||
|
8aa65f28c6
|
|||
|
f9edec7e41
|
|||
|
305c600cf5
|
|||
|
8dd3e1ee5d
|
|||
|
4ffeec3004
|
|||
|
9ed3ba85ea
|
|||
|
4433c993fa
|
|||
|
430991c39b
|
|||
|
ba3227bf15
|
|||
|
0e543a58b3
|
|||
|
c989e7785a
|
|||
|
332d90d6c7
|
|||
|
99ac96511b
|
|||
|
e99d7affb0
|
|||
|
41ac2be965
|
|||
|
02271583fb
|
|||
|
ef54b2cd08
|
|||
|
82608164f6
|
|||
|
edd6f2cfa9
|
|||
|
acffa76812
|
|||
|
8da76483e6
|
|||
|
534c932906
|
|||
|
fee10fed4d
|
|||
|
a4f7e92e1c
|
|||
|
f1a53d6116
|
|||
|
b353c3deea
|
|||
|
fde5f1ca64
|
|||
|
4d0bdd84b5
|
|||
|
72a931a71a
|
|||
|
9a25542c6d
|
|||
|
c6be82bcf9
|
|||
|
38245559dc
|
|||
|
7b416d47dc
|
|||
|
15170735ba
|
|||
|
6a3886e9db
|
|||
|
ff66296378
|
|||
|
347a79df72
|
|||
|
0f78864a67
|
|||
|
b32b1975a8
|
|||
|
2b1eaa62f1
|
|||
|
f13dca184c
|
|||
|
3b8a3d3b00
|
|||
|
c5d24979f5
|
|||
|
1dc780bca7
|
|||
|
ec33061c92
|
|||
|
af0899de96
|
|||
|
547a2adaa4
|
|||
|
c02948e155
|
|||
|
387b86bcdd
|
|||
|
4e85643865
|
|||
|
987981df73
|
|||
|
f14e7255be
|
|||
|
a8a79a8664
|
|||
|
3ae0cec000
|
|||
|
4e518f11d8
|
|||
|
cb513bb1cd
|
|||
|
f7bd28118c
|
|||
|
940ee00ffe
|
|||
|
b43d104680
|
|||
|
ddf48a6c22
|
|||
|
a0f499e30a
|
|||
|
d6b07f12ff
|
|||
|
65fe09caf9
|
|||
|
a1e5f020f4
|
|||
|
bd3fa53a55
|
|||
|
625632c593
|
|||
|
e71ae3b8c5
|
|||
|
9d7a19d162
|
|||
|
6ba19a7ba5
|
|||
|
749a2779f5
|
|||
|
e574042d76
|
|||
|
2b44493e8a
|
|||
|
c30dd4e630
|
|||
|
d90da1c8f5
|
|||
|
5853d7700f
|
|||
|
d5c7523726
|
|||
|
ddfcc51b91
|
|||
|
8ebedbd88a
|
|||
|
84e8142a2d
|
|||
|
2c7b7ad845
|
|||
|
72c2b66fc0
|
|||
|
356b42a406
|
|||
|
d9b6d48e7c
|
|||
|
087959e81b
|
|||
|
e6967b8bbb
|
|||
|
d2f9a9b83b
|
|||
|
1b5ecd9eaf
|
|||
|
82561d62b6
|
|||
|
eec021cc4b
|
|||
|
a1d98823f8
|
|||
|
255b77d91d
|
|||
|
f84ec5a3f8
|
|||
|
eb22a8bcc1
|
|||
|
31aef905fa
|
|||
|
a6887f7253
|
|||
|
69bd581af7
|
|||
|
26b7afc890
|
|||
|
d5532aade0
|
|||
|
0c5409aec7
|
|||
|
1a8840bebc
|
|||
|
1fb453dffe
|
|||
|
e03d702d08
|
|||
|
241dc964a6
|
|||
|
8ef71e14d5
|
|||
|
972f4006f0
|
|||
|
9a8a047908
|
|||
|
863bf69ad3
|
|||
|
0e957cc9c1
|
|||
|
aa454b158f
|
|||
|
7007bd6a1c
|
|||
|
00efc95ee7
|
|||
|
b380bb248c
|
|||
|
87e008d56d
|
|||
|
3992073212
|
|||
|
ef80b19f2f
|
|||
|
717771ae80
|
|||
|
bf5772bd8a
|
|||
|
9a7c81a44e
|
|||
|
b7e991de5b
|
|||
|
6c1205106d
|
|||
|
2ffca6984a
|
|||
|
dde2516304
|
|||
|
f30a439bcd
|
|||
|
008e9e7fc5
|
|||
|
23aefcd759
|
|||
|
cb8b886446
|
|||
|
5979d8b1e0
|
|||
|
e587112e63
|
|||
|
d6cf736abf
|
|||
|
15011c4173
|
|||
|
31b7ddd122
|
|||
|
c460892cbd
|
|||
|
6309469e93
|
|||
|
0d7c1a9a43
|
|||
|
ae6f5ede19
|
|||
|
807d511c8b
|
|||
|
2f4f21fb18
|
|||
|
9967909460
|
|||
|
c806f43881
|
|||
|
584405f7cc
|
|||
|
50127ed5f9
|
|||
|
b5eff27c40
|
|||
|
74ba183256
|
|||
|
f885dede9b
|
|||
|
e9a7cd526f
|
|||
|
12be7bc78e
|
|||
|
0ba8be659f
|
|||
|
022242a84a
|
|||
|
8aeb06f53c
|
|||
|
4036da3b5c
|
|||
|
986105958c
|
|||
|
ecdd4d8202
|
|||
|
bdee0c3921
|
|||
|
48f634d046
|
|||
|
2a46f5bb12
|
|||
|
7f2c0af5ad
|
|||
|
297b444dfb
|
|||
|
89a05909a4
|
|||
|
f772940768
|
|||
|
8886c40974
|
|||
|
8b62e08b44
|
|||
|
72c59f9229
|
|||
|
ff3cfbb437
|
|||
|
c13eb70d7d
|
|||
|
389402f955
|
|||
|
660a2898dc
|
|||
|
faf59e12c0
|
|||
|
d97a03c7c6
|
|||
|
a102178019
|
|||
|
e400862a12
|
|||
|
184e9db2b2
|
|||
|
605d018be2
|
|||
|
78aaae7ee0
|
|||
|
5c82f1ed3e
|
|||
|
f8502c3ece
|
|||
|
996b42634d
|
|||
|
300571af47
|
|||
|
32c90ef4e7
|
|||
|
2a4e2724a3
|
|||
|
d613257841
|
|||
|
18644d90be
|
|||
|
52fcc48ac1
|
|||
|
8b69bcd215
|
|||
|
2dd49c437c
|
|||
|
92852d8235
|
|||
|
371dd5b938
|
|||
|
4836d570ae
|
|||
|
985f9442e6
|
|||
|
67eb28466d
|
|||
|
c326c3f97d
|
|||
|
971c79bb80
|
|||
|
f86d868274
|
|||
|
33940265a6
|
|||
|
b39f3aeb59
|
|||
|
61dbfeffe7
|
|||
|
532feb4bfa
|
|||
|
ec5e91b8c9
|
|||
|
ee51320abf
|
|||
|
5c4058d5ac
|
|||
|
e732dca762
|
|||
|
a9adcd914b
|
|||
|
3dd4ff29c8
|
|||
|
61d86c5e10
|
|||
|
d097eaa28f
|
|||
|
ad3576c164
|
|||
|
b989a4601a
|
|||
|
a11237b158
|
|||
|
40f00d570e
|
|||
|
0eb1bc6301
|
|||
|
1eb837eab8
|
|||
|
0a4e633db2
|
|||
|
e8809125d4
|
|||
|
806ce18c0a
|
|||
|
b71d2bf534
|
|||
|
46059b1840
|
|||
|
d2c329bcea
|
|||
|
2d379b5a38
|
|||
|
75e0c5d406
|
|||
|
770b37ae16
|
|||
|
c638193268
|
|||
|
8c3a817881
|
|||
|
e2fce321c1
|
|||
|
241702ae3a
|
|||
|
d21d9c5b1d
|
|||
|
a70daf2250
|
|||
|
632b18addd
|
|||
|
a57a7a6a16
|
|||
|
5098b12e4a
|
|||
|
9ddf5794dd
|
|||
|
b74a08dda9
|
|||
|
1b9408864f
|
|||
|
cc89dbdf63
|
|||
|
228f3301f2
|
|||
|
07181138e5
|
|||
|
816b372f14
|
|||
|
d7eddd54a2
|
|||
|
7c063833e0
|
|||
|
af3619d440
|
|||
|
528674cb6e
|
|||
|
70c9757e26
|
|||
|
c83a7e2efc
|
|||
|
904208b87f
|
|||
|
007b52d81f
|
|||
|
3385538142
|
|||
|
24618ab9a1
|
|||
|
9ce4706a07
|
|||
|
9a1f8e129f
|
|||
|
ee10860357
|
|||
|
44277dc0f1
|
|||
|
bc54db54d2
|
|||
|
bf07b7cd9e
|
|||
|
5d3c8dcc92
|
|||
|
48feca800f
|
|||
|
42de09e896
|
|||
|
1576fea8a3
|
|||
|
ae522ab364
|
|||
|
273d97af85
|
|||
|
891316d924
|
|||
|
9f5dad1998
|
|||
|
6e7ddb2d2e
|
|||
|
bac4e67867
|
|||
|
4230281194
|
|||
|
e64e7608ca
|
|||
|
10a21ce3ef
|
|||
|
0f1f0e4364
|
|||
|
f9bf20a3c7
|
|||
|
73c1a83032
|
|||
|
f443d315ad
|
|||
|
9e18d1de77
|
|||
|
2647a71be1
|
|||
|
7c60a4d8e8
|
|||
|
4bb5d9780f
|
|||
|
f41fd94628
|
|||
|
94895bbacb
|
|||
|
f332200ca4
|
|||
|
2eff470091
|
|||
|
a092b042ab
|
|||
|
e94b09d337
|
|||
|
5d9e669d97
|
|||
|
f1002157a5
|
|||
|
4133b555ba
|
|||
|
9b1a60b5c9
|
|||
|
beb3918809
|
|||
|
2871426df2
|
|||
|
e048f31baa
|
|||
|
6af8b8859f
|
|||
|
f38ba7e923
|
|||
|
d22145a392
|
|||
|
29c3f8becb
|
|||
|
be16970e77
|
|||
|
df266527f1
|
|||
|
c8ed7aae6e
|
|||
|
61e58aa14d
|
|||
|
9e15898c8f
|
|||
|
f7bd6a5a41
|
|||
|
ea853e21d9
|
|||
|
0bd9b9e8fe
|
|||
|
39e32799b3
|
|||
|
9953768de5
|
|||
|
0d3652b793
|
|||
|
d8e9d71f87
|
|||
|
558974b996
|
|||
|
4de4049713
|
|||
|
2d4cabe786
|
|||
|
80f9b62d25
|
|||
|
673b648bd3
|
|||
|
45ad788c6d
|
|||
|
56539d8db5
|
|||
|
840ceb615a
|
|||
|
741d011543
|
|||
|
d050b3de25
|
|||
|
5de28800ad
|
|||
|
8e50293ab7
|
|||
|
12c6d66bfd
|
|||
|
d7d2bd33ed
|
|||
|
c21a4cff14
|
|||
|
4fa38d6063
|
|||
|
6d4ac3d9fd
|
|||
|
a5d2f040fb
|
|||
|
c62689e17f
|
|||
|
39dc8e7bd8
|
|||
|
5a732d153e
|
|||
|
b4549c72be
|
|||
|
1818dc3a4c
|
|||
|
65094b63cd
|
|||
|
f0a082ec84
|
|||
|
751aa350ee
|
|||
|
e6cd2bb2a8
|
|||
|
0fb72e5d99
|
|||
|
71135f339a
|
|||
|
b6af8caffe
|
|||
|
e1a3549ea0
|
|||
|
8bf162820b
|
|||
|
dccb366608
|
|||
|
83c8f0488b
|
|||
|
478b27922c
|
|||
|
ba1498cd18
|
|||
|
eda4d612c2
|
|||
|
2e7e160683
|
|||
|
79957f8ea7
|
|||
|
7e52463445
|
|||
|
89970f5197
|
|||
|
35037705a9
|
|||
|
647c6ea21b
|
|||
|
416d93e880
|
|||
|
312753924b
|
|||
|
54308f79d2
|
|||
|
dfa3217037
|
|||
|
8000a2febb
|
|||
|
7bd48d3489
|
|||
|
b5eaeac11a
|
|||
|
a9986aab6a
|
|||
|
ff30a5ab5d
|
|||
|
eb0c16dd8c
|
|||
|
4fa1e97026
|
|||
|
64b6dc41ba
|
|||
|
c64b8163e7
|
|||
|
9d9a165379
|
|||
|
d0dff1cac9
|
|||
|
3c80fd2b0f
|
|||
|
ef81828e0c
|
|||
|
2978a6f046
|
|||
|
dfd9467523
|
|||
|
53571f030e
|
|||
|
aa164081e1
|
|||
|
9a10eeab90
|
|||
|
d1f83f40d6
|
|||
|
a748d40745
|
|||
|
648e1d641a
|
|||
|
3c327084d3
|
|||
|
ffaa12b9d8
|
|||
|
bf95127332
|
|||
|
e0f321b2c4
|
|||
|
2c9c7fee5b
|
|||
|
d0400f3c81
|
|||
|
e9b0f9faef
|
|||
|
e85be67fd9
|
|||
|
7e69893264
|
|||
|
38a3e6af03
|
|||
|
90cb01b274
|
|||
|
b1e1d5627e
|
|||
|
3ae2ab652e
|
|||
|
db71fbe22b
|
|||
|
83e72c2b59
|
|||
|
82a072f641
|
|||
|
60c10c3f4a
|
|||
|
468696f611
|
|||
|
29c38caac8
|
|||
|
e599b5583d
|
|||
|
33a4ab11c2
|
|||
|
1fa5e992e4
|
|||
|
c667b13a00
|
@@ -20,5 +20,5 @@ jobs:
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: |-
|
||||
result/fortify-**
|
||||
result/hakurei-**
|
||||
api_key: '${{secrets.RELEASE_TOKEN}}'
|
||||
|
||||
@@ -5,25 +5,107 @@ on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run NixOS test
|
||||
hakurei:
|
||||
name: Hakurei
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
nix --print-build-logs --experimental-features 'nix-command flakes' flake check
|
||||
nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.nixos-tests
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hakurei
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "nixos-vm-output"
|
||||
name: "hakurei-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
race:
|
||||
name: Hakurei (race detector)
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "hakurei-race-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
sandbox:
|
||||
name: Sandbox
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "sandbox-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
sandbox-race:
|
||||
name: Sandbox (race detector)
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox-race
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "sandbox-race-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
hpkg:
|
||||
name: Hpkg
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hpkg
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "hpkg-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
check:
|
||||
name: Flake checks
|
||||
needs:
|
||||
- hakurei
|
||||
- race
|
||||
- sandbox
|
||||
- sandbox-race
|
||||
- hpkg
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run checks
|
||||
run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check
|
||||
|
||||
dist:
|
||||
name: Create distribution
|
||||
runs-on: nix
|
||||
@@ -34,15 +116,15 @@ jobs:
|
||||
- name: Build for test
|
||||
id: build-test
|
||||
run: >-
|
||||
export FORTIFY_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$FORTIFY_REV'"; # version = /' package.nix &&
|
||||
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
|
||||
nix build --print-out-paths --print-build-logs .#dist &&
|
||||
mv package.nix.old package.nix &&
|
||||
echo "rev=$FORTIFY_REV" >> $GITHUB_OUTPUT
|
||||
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload test build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "fortify-${{ steps.build-test.outputs.rev }}"
|
||||
name: "hakurei-${{ steps.build-test.outputs.rev }}"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
5
.github/workflows/README
vendored
Normal file
5
.github/workflows/README
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
DO NOT ADD NEW ACTIONS HERE
|
||||
|
||||
This port is solely for releasing to the github mirror and serves no purpose during development.
|
||||
All development happens at https://git.gensokyo.uk/security/hakurei. If you wish to contribute,
|
||||
request for an account on git.gensokyo.uk.
|
||||
46
.github/workflows/release.yml
vendored
Normal file
46
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: nixbuild/nix-quick-install-action@v32
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and cache Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
||||
restore-prefixes-first-match: build-${{ runner.os }}-
|
||||
gc-max-store-size-linux: 1G
|
||||
purge: true
|
||||
purge-prefixes: build-${{ runner.os }}-
|
||||
purge-created: 60
|
||||
purge-primary-key: never
|
||||
|
||||
- name: Build for release
|
||||
run: nix build --print-out-paths --print-build-logs .#dist
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |-
|
||||
result/hakurei-**
|
||||
48
.github/workflows/test.yml
vendored
Normal file
48
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
- push
|
||||
|
||||
jobs:
|
||||
dist:
|
||||
name: Create distribution
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: nixbuild/nix-quick-install-action@v32
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and cache Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
||||
restore-prefixes-first-match: build-${{ runner.os }}-
|
||||
gc-max-store-size-linux: 1G
|
||||
purge: true
|
||||
purge-prefixes: build-${{ runner.os }}-
|
||||
purge-created: 60
|
||||
purge-primary-key: never
|
||||
|
||||
- name: Build for test
|
||||
id: build-test
|
||||
run: >-
|
||||
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
|
||||
nix build --print-out-paths --print-build-logs .#dist &&
|
||||
mv package.nix.old package.nix &&
|
||||
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload test build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "hakurei-${{ steps.build-test.outputs.rev }}"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -5,7 +5,7 @@
|
||||
*.so
|
||||
*.dylib
|
||||
*.pkg
|
||||
/fortify
|
||||
/hakurei
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
@@ -26,7 +26,10 @@ go.work.sum
|
||||
.vscode
|
||||
|
||||
# go generate
|
||||
security-context-v1-protocol.*
|
||||
/cmd/hakurei/LICENSE
|
||||
|
||||
# release
|
||||
/dist/fortify-*
|
||||
/dist/hakurei-*
|
||||
|
||||
# interactive nixos vm
|
||||
nixos.qcow2
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2024 Ophestra Umiker
|
||||
Copyright (c) 2024-2025 Ophestra
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
110
README.md
110
README.md
@@ -1,77 +1,84 @@
|
||||
Fortify
|
||||
=======
|
||||
<p align="center">
|
||||
<a href="https://git.gensokyo.uk/security/hakurei">
|
||||
<picture>
|
||||
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](https://pkg.go.dev/git.gensokyo.uk/security/fortify)
|
||||
[](https://goreportcard.com/report/git.gensokyo.uk/security/fortify)
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
|
||||
<a href="https://git.gensokyo.uk/security/hakurei/actions"><img src="https://git.gensokyo.uk/security/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
|
||||
<br/>
|
||||
<a href="https://git.gensokyo.uk/security/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/security/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
|
||||
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
|
||||
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
|
||||
</p>
|
||||
|
||||
Lets you run graphical applications as another user in a confined environment with a nice NixOS
|
||||
module to configure target users and provide launchers and desktop files for your privileged user.
|
||||
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
|
||||
It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette),
|
||||
a self-contained Android-like package manager with modern security features.
|
||||
|
||||
Why would you want this?
|
||||
## NixOS Module usage
|
||||
|
||||
- It protects the desktop environment from applications.
|
||||
|
||||
- It protects applications from each other.
|
||||
|
||||
- It provides UID isolation on top of the standard application sandbox.
|
||||
|
||||
If you have a flakes-enabled nix environment, you can try out the tool by running:
|
||||
|
||||
```shell
|
||||
nix run git+https://git.gensokyo.uk/security/fortify -- help
|
||||
```
|
||||
|
||||
## Module usage
|
||||
|
||||
The NixOS module currently requires home-manager to function correctly.
|
||||
|
||||
Full module documentation can be found [here](options.md).
|
||||
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
|
||||
|
||||
To use the module, import it into your configuration with
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
|
||||
fortify = {
|
||||
url = "git+https://git.gensokyo.uk/security/fortify";
|
||||
hakurei = {
|
||||
url = "git+https://git.gensokyo.uk/security/hakurei";
|
||||
|
||||
# Optional but recommended to limit the size of your system closure.
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, fortify, ... }:
|
||||
outputs = { self, nixpkgs, hakurei, ... }:
|
||||
{
|
||||
nixosConfigurations.fortify = nixpkgs.lib.nixosSystem {
|
||||
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
fortify.nixosModules.fortify
|
||||
hakurei.nixosModules.hakurei
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This adds the `environment.fortify` option:
|
||||
This adds the `environment.hakurei` option:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
environment.fortify = {
|
||||
environment.hakurei = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/persist/module/fortify";
|
||||
stateDir = "/var/lib/hakurei";
|
||||
users = {
|
||||
alice = 0;
|
||||
nixos = 10;
|
||||
};
|
||||
|
||||
apps = [
|
||||
commonPaths = [
|
||||
{
|
||||
src = "/sdcard";
|
||||
write = true;
|
||||
}
|
||||
];
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
|
||||
apps = {
|
||||
"org.chromium.Chromium" = {
|
||||
name = "chromium";
|
||||
id = "org.chromium.Chromium";
|
||||
identity = 1;
|
||||
packages = [ pkgs.chromium ];
|
||||
userns = true;
|
||||
mapRealUid = true;
|
||||
@@ -104,16 +111,20 @@ This adds the `environment.fortify` option:
|
||||
broadcast = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"org.claws_mail.Claws-Mail" = {
|
||||
name = "claws-mail";
|
||||
id = "org.claws_mail.Claws-Mail";
|
||||
identity = 2;
|
||||
packages = [ pkgs.claws-mail ];
|
||||
gpu = false;
|
||||
capability.pulse = false;
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"org.weechat" = {
|
||||
name = "weechat";
|
||||
identity = 3;
|
||||
shareUid = true;
|
||||
packages = [ pkgs.weechat ];
|
||||
capability = {
|
||||
wayland = false;
|
||||
@@ -121,10 +132,12 @@ This adds the `environment.fortify` option:
|
||||
dbus = true;
|
||||
pulse = false;
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"dev.vencord.Vesktop" = {
|
||||
name = "discord";
|
||||
id = "dev.vencord.Vesktop";
|
||||
identity = 3;
|
||||
shareUid = true;
|
||||
packages = [ pkgs.vesktop ];
|
||||
share = pkgs.vesktop;
|
||||
command = "vesktop --ozone-platform-hint=wayland";
|
||||
@@ -142,9 +155,12 @@ This adds the `environment.fortify` option:
|
||||
};
|
||||
system.filter = true;
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"io.looking-glass" = {
|
||||
name = "looking-glass-client";
|
||||
identity = 4;
|
||||
useCommonPaths = false;
|
||||
groups = [ "plugdev" ];
|
||||
extraPaths = [
|
||||
{
|
||||
@@ -155,8 +171,8 @@ This adds the `environment.fortify` option:
|
||||
extraConfig = {
|
||||
programs.looking-glass-client.enable = true;
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
196
acl/c.go
196
acl/c.go
@@ -1,196 +0,0 @@
|
||||
package acl
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: --static libacl
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <sys/acl.h>
|
||||
#include <acl/libacl.h>
|
||||
|
||||
static acl_t _go_acl_get_file(const char *path_p, acl_type_t type) {
|
||||
acl_t acl = acl_get_file(path_p, type);
|
||||
free((void *)path_p);
|
||||
return acl;
|
||||
}
|
||||
|
||||
static int _go_acl_set_file(const char *path_p, acl_type_t type, acl_t acl) {
|
||||
if (acl_valid(acl) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = acl_set_file(path_p, type, acl);
|
||||
free((void *)path_p);
|
||||
return ret;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func getFile(name string, t C.acl_type_t) (*ACL, error) {
|
||||
a, err := C._go_acl_get_file(C.CString(name), t)
|
||||
if errors.Is(err, syscall.ENODATA) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return newACL(a), err
|
||||
}
|
||||
|
||||
func (acl *ACL) setFile(name string, t C.acl_type_t) error {
|
||||
_, err := C._go_acl_set_file(C.CString(name), t, acl.acl)
|
||||
return err
|
||||
}
|
||||
|
||||
func newACL(a C.acl_t) *ACL {
|
||||
acl := &ACL{a}
|
||||
runtime.SetFinalizer(acl, (*ACL).free)
|
||||
return acl
|
||||
}
|
||||
|
||||
type ACL struct {
|
||||
acl C.acl_t
|
||||
}
|
||||
|
||||
func (acl *ACL) free() {
|
||||
C.acl_free(unsafe.Pointer(acl.acl))
|
||||
|
||||
// no need for a finalizer anymore
|
||||
runtime.SetFinalizer(acl, nil)
|
||||
}
|
||||
|
||||
const (
|
||||
Read = C.ACL_READ
|
||||
Write = C.ACL_WRITE
|
||||
Execute = C.ACL_EXECUTE
|
||||
|
||||
TypeDefault = C.ACL_TYPE_DEFAULT
|
||||
TypeAccess = C.ACL_TYPE_ACCESS
|
||||
|
||||
UndefinedTag = C.ACL_UNDEFINED_TAG
|
||||
UserObj = C.ACL_USER_OBJ
|
||||
User = C.ACL_USER
|
||||
GroupObj = C.ACL_GROUP_OBJ
|
||||
Group = C.ACL_GROUP
|
||||
Mask = C.ACL_MASK
|
||||
Other = C.ACL_OTHER
|
||||
)
|
||||
|
||||
type (
|
||||
Perm C.acl_perm_t
|
||||
)
|
||||
|
||||
func (acl *ACL) removeEntry(tt C.acl_tag_t, tq int) error {
|
||||
var e C.acl_entry_t
|
||||
|
||||
// get first entry
|
||||
if r, err := C.acl_get_entry(acl.acl, C.ACL_FIRST_ENTRY, &e); err != nil {
|
||||
return err
|
||||
} else if r == 0 {
|
||||
// return on acl with no entries
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
if r, err := C.acl_get_entry(acl.acl, C.ACL_NEXT_ENTRY, &e); err != nil {
|
||||
return err
|
||||
} else if r == 0 {
|
||||
// return on drained acl
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
q int
|
||||
t C.acl_tag_t
|
||||
)
|
||||
|
||||
// get current entry tag type
|
||||
if _, err := C.acl_get_tag_type(e, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get current entry qualifier
|
||||
if rq, err := C.acl_get_qualifier(e); err != nil {
|
||||
// neither ACL_USER nor ACL_GROUP
|
||||
if errors.Is(err, syscall.EINVAL) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
} else {
|
||||
q = *(*int)(rq)
|
||||
C.acl_free(rq)
|
||||
}
|
||||
|
||||
// delete on match
|
||||
if t == tt && q == tq {
|
||||
_, err := C.acl_delete_entry(acl.acl, e)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UpdatePerm(name string, uid int, perms ...Perm) error {
|
||||
// read acl from file
|
||||
a, err := getFile(name, TypeAccess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// free acl on return if get is successful
|
||||
defer a.free()
|
||||
|
||||
// remove existing entry
|
||||
if err = a.removeEntry(User, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new entry if perms are passed
|
||||
if len(perms) > 0 {
|
||||
// create new acl entry
|
||||
var e C.acl_entry_t
|
||||
if _, err = C.acl_create_entry(&a.acl, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get perm set of new entry
|
||||
var p C.acl_permset_t
|
||||
if _, err = C.acl_get_permset(e, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add target perms
|
||||
for _, perm := range perms {
|
||||
if _, err = C.acl_add_perm(p, C.acl_perm_t(perm)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set perm set to new entry
|
||||
if _, err = C.acl_set_permset(e, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set user tag to new entry
|
||||
if _, err = C.acl_set_tag_type(e, User); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set qualifier (uid) to new entry
|
||||
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// calculate mask after update
|
||||
if _, err = C.acl_calc_mask(&a.acl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write acl to file
|
||||
return a.setFile(name, TypeAccess)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/system"
|
||||
)
|
||||
|
||||
type bundleInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
|
||||
// passed through to [fst.Config]
|
||||
ID string `json:"id"`
|
||||
// passed through to [fst.Config]
|
||||
AppID int `json:"app_id"`
|
||||
// passed through to [fst.Config]
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
UserNS bool `json:"userns,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
Net bool `json:"net,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
Dev bool `json:"dev,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
NoNewSession bool `json:"no_new_session,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
MapRealUID bool `json:"map_real_uid,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
Enablements system.Enablements `json:"enablements"`
|
||||
|
||||
// passed through inverted to [bwrap.SyscallPolicy]
|
||||
Devel bool `json:"devel,omitempty"`
|
||||
// passed through to [bwrap.SyscallPolicy]
|
||||
Multiarch bool `json:"multiarch,omitempty"`
|
||||
// passed through to [bwrap.SyscallPolicy]
|
||||
Bluetooth bool `json:"bluetooth,omitempty"`
|
||||
|
||||
// allow gpu access within sandbox
|
||||
GPU bool `json:"gpu"`
|
||||
// store path to nixGL mesa wrappers
|
||||
Mesa string `json:"mesa,omitempty"`
|
||||
// store path to nixGL source
|
||||
NixGL string `json:"nix_gl,omitempty"`
|
||||
// store path to activate-and-exec script
|
||||
Launcher string `json:"launcher"`
|
||||
// store path to /run/current-system
|
||||
CurrentSystem string `json:"current_system"`
|
||||
// store path to home-manager activation package
|
||||
ActivationPackage string `json:"activation_package"`
|
||||
}
|
||||
|
||||
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
|
||||
bundle := new(bundleInfo)
|
||||
if f, err := os.Open(name); err != nil {
|
||||
beforeFail()
|
||||
fmsg.Fatalf("cannot open bundle: %v", err)
|
||||
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
|
||||
beforeFail()
|
||||
fmsg.Fatalf("cannot parse bundle metadata: %v", err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
fmsg.Printf("cannot close bundle metadata: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if bundle.ID == "" {
|
||||
beforeFail()
|
||||
fmsg.Fatal("application identifier must not be empty")
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
func formatHostname(name string) string {
|
||||
if h, err := os.Hostname(); err != nil {
|
||||
fmsg.Printf("cannot get hostname: %v", err)
|
||||
return "fortify-" + name
|
||||
} else {
|
||||
return h + "-" + name
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func actionInstall(args []string) {
|
||||
set := flag.NewFlagSet("install", flag.ExitOnError)
|
||||
var (
|
||||
dropShellInstall bool
|
||||
dropShellActivate bool
|
||||
)
|
||||
set.BoolVar(&dropShellInstall, "si", false, "Drop to a shell on installation")
|
||||
set.BoolVar(&dropShellActivate, "sa", false, "Drop to a shell on activation")
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args)
|
||||
|
||||
args = set.Args()
|
||||
|
||||
if len(args) != 1 {
|
||||
fmsg.Fatal("invalid argument")
|
||||
}
|
||||
pkgPath := args[0]
|
||||
if !path.IsAbs(pkgPath) {
|
||||
if dir, err := os.Getwd(); err != nil {
|
||||
fmsg.Fatalf("cannot get current directory: %v", err)
|
||||
} else {
|
||||
pkgPath = path.Join(dir, pkgPath)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Look up paths to programs started by fpkg.
|
||||
This is done here to ease error handling as cleanup is not yet required.
|
||||
*/
|
||||
|
||||
var (
|
||||
_ = lookPath("zstd")
|
||||
tar = lookPath("tar")
|
||||
chmod = lookPath("chmod")
|
||||
rm = lookPath("rm")
|
||||
)
|
||||
|
||||
/*
|
||||
Extract package and set up for cleanup.
|
||||
*/
|
||||
|
||||
var workDir string
|
||||
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
|
||||
fmsg.Fatalf("cannot create temporary directory: %v", err)
|
||||
} else {
|
||||
workDir = p
|
||||
}
|
||||
cleanup := func() {
|
||||
// should be faster than a native implementation
|
||||
mustRun(chmod, "-R", "+w", workDir)
|
||||
mustRun(rm, "-rf", workDir)
|
||||
}
|
||||
beforeRunFail.Store(&cleanup)
|
||||
|
||||
mustRun(tar, "-C", workDir, "-xf", pkgPath)
|
||||
|
||||
/*
|
||||
Parse bundle and app metadata, do pre-install checks.
|
||||
*/
|
||||
|
||||
bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
|
||||
pathSet := pathSetByApp(bundle.ID)
|
||||
|
||||
app := bundle
|
||||
if s, err := os.Stat(pathSet.metaPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
cleanup()
|
||||
fmsg.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
|
||||
panic("unreachable")
|
||||
}
|
||||
// did not modify app, clean installation condition met later
|
||||
} else if s.IsDir() {
|
||||
cleanup()
|
||||
fmsg.Fatalf("metadata path %q is not a file", pathSet.metaPath)
|
||||
panic("unreachable")
|
||||
} else {
|
||||
app = loadBundleInfo(pathSet.metaPath, cleanup)
|
||||
if app.ID != bundle.ID {
|
||||
cleanup()
|
||||
fmsg.Fatalf("app %q claims to have identifier %q", bundle.ID, app.ID)
|
||||
}
|
||||
// sec: should verify credentials
|
||||
}
|
||||
|
||||
if app != bundle {
|
||||
// do not try to re-install
|
||||
if app.NixGL == bundle.NixGL &&
|
||||
app.CurrentSystem == bundle.CurrentSystem &&
|
||||
app.Launcher == bundle.Launcher &&
|
||||
app.ActivationPackage == bundle.ActivationPackage {
|
||||
cleanup()
|
||||
fmsg.Printf("package %q is identical to local application %q", pkgPath, app.ID)
|
||||
fmsg.Exit(0)
|
||||
}
|
||||
|
||||
// AppID determines uid
|
||||
if app.AppID != bundle.AppID {
|
||||
cleanup()
|
||||
fmsg.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// sec: should compare version string
|
||||
fmsg.VPrintf("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
|
||||
} else {
|
||||
fmsg.VPrintf("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
/*
|
||||
Setup steps for files owned by the target user.
|
||||
*/
|
||||
|
||||
withCacheDir("install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + fst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
"mkdir -p etc",
|
||||
"chmod -R +w etc",
|
||||
"rm -rf etc",
|
||||
"cp -dRf $BUNDLE/etc etc",
|
||||
// replace inner /nix
|
||||
"mkdir -p nix",
|
||||
"chmod -R +w nix",
|
||||
"rm -rf nix",
|
||||
"cp -dRf /nix nix",
|
||||
// copy from binary cache
|
||||
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
|
||||
// deduplicate nix store
|
||||
"nix store --offline --store $PWD optimise",
|
||||
// make cache directory world-readable for autoetc
|
||||
"chmod 0755 .",
|
||||
}, workDir, bundle, pathSet, dropShellInstall, cleanup)
|
||||
|
||||
if bundle.GPU {
|
||||
withCacheDir("mesa-wrappers", []string{
|
||||
// link nixGL mesa wrappers
|
||||
"mkdir -p nix/.nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
|
||||
}, workDir, bundle, pathSet, false, cleanup)
|
||||
}
|
||||
|
||||
/*
|
||||
Activate home-manager generation.
|
||||
*/
|
||||
|
||||
withNixDaemon("activate", []string{
|
||||
// clean up broken links
|
||||
"mkdir -p .local/state/{nix,home-manager}",
|
||||
"chmod -R +w .local/state/{nix,home-manager}",
|
||||
"rm -rf .local/state/{nix,home-manager}",
|
||||
// run activation script
|
||||
bundle.ActivationPackage + "/activate",
|
||||
}, false, func(config *fst.Config) *fst.Config { return config }, bundle, pathSet, dropShellActivate, cleanup)
|
||||
|
||||
/*
|
||||
Installation complete. Write metadata to block re-installs or downgrades.
|
||||
*/
|
||||
|
||||
// serialise metadata to ensure consistency
|
||||
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||
cleanup()
|
||||
fmsg.Fatalf("cannot create metadata file: %v", err)
|
||||
panic("unreachable")
|
||||
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
|
||||
cleanup()
|
||||
fmsg.Fatalf("cannot write metadata: %v", err)
|
||||
panic("unreachable")
|
||||
} else if err = f.Close(); err != nil {
|
||||
fmsg.Printf("cannot close metadata file: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
|
||||
cleanup()
|
||||
fmsg.Fatalf("cannot rename metadata file: %v", err)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
cleanup()
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
const shell = "/run/current-system/sw/bin/bash"
|
||||
|
||||
func init() {
|
||||
if err := os.Setenv("SHELL", shell); err != nil {
|
||||
fmsg.Fatalf("cannot set $SHELL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
flagVerbose bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmsg.SetPrefix("fpkg")
|
||||
|
||||
flag.Parse()
|
||||
fmsg.SetVerbose(flagVerbose)
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) < 1 {
|
||||
fmsg.Fatal("invalid argument")
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "install":
|
||||
actionInstall(args[1:])
|
||||
case "start":
|
||||
actionStart(args[1:])
|
||||
|
||||
default:
|
||||
fmsg.Fatal("invalid argument")
|
||||
}
|
||||
|
||||
fmsg.Exit(0)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
var (
|
||||
dataHome string
|
||||
)
|
||||
|
||||
func init() {
|
||||
// dataHome
|
||||
if p, ok := os.LookupEnv("FORTIFY_DATA_HOME"); ok {
|
||||
dataHome = p
|
||||
} else {
|
||||
dataHome = "/var/lib/fortify/" + strconv.Itoa(os.Getuid())
|
||||
}
|
||||
}
|
||||
|
||||
func lookPath(file string) string {
|
||||
if p, err := exec.LookPath(file); err != nil {
|
||||
fmsg.Fatalf("%s: command not found", file)
|
||||
panic("unreachable")
|
||||
} else {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
var beforeRunFail = new(atomic.Pointer[func()])
|
||||
|
||||
func mustRun(name string, arg ...string) {
|
||||
fmsg.VPrintf("spawning process: %q %q", name, arg)
|
||||
cmd := exec.Command(name, arg...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if f := beforeRunFail.Swap(nil); f != nil {
|
||||
(*f)()
|
||||
}
|
||||
fmsg.Fatalf("%s: %v", name, err)
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
type appPathSet struct {
|
||||
// ${dataHome}/${id}
|
||||
baseDir string
|
||||
// ${baseDir}/app
|
||||
metaPath string
|
||||
// ${baseDir}/files
|
||||
homeDir string
|
||||
// ${baseDir}/cache
|
||||
cacheDir string
|
||||
// ${baseDir}/cache/nix
|
||||
nixPath string
|
||||
}
|
||||
|
||||
func pathSetByApp(id string) *appPathSet {
|
||||
pathSet := new(appPathSet)
|
||||
pathSet.baseDir = path.Join(dataHome, id)
|
||||
pathSet.metaPath = path.Join(pathSet.baseDir, "app")
|
||||
pathSet.homeDir = path.Join(pathSet.baseDir, "files")
|
||||
pathSet.cacheDir = path.Join(pathSet.baseDir, "cache")
|
||||
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
|
||||
return pathSet
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
|
||||
var (
|
||||
Fmain = compPoison
|
||||
)
|
||||
|
||||
func fortifyApp(config *fst.Config, beforeFail func()) {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
st io.WriteCloser
|
||||
)
|
||||
if p, ok := internal.Path(Fmain); !ok {
|
||||
beforeFail()
|
||||
fmsg.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
|
||||
panic("unreachable")
|
||||
} else if r, w, err := os.Pipe(); err != nil {
|
||||
beforeFail()
|
||||
fmsg.Fatalf("cannot pipe: %v", err)
|
||||
panic("unreachable")
|
||||
} else {
|
||||
if fmsg.Verbose() {
|
||||
cmd = exec.Command(p, "-v", "app", "3")
|
||||
} else {
|
||||
cmd = exec.Command(p, "app", "3")
|
||||
}
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.ExtraFiles = []*os.File{r}
|
||||
st = w
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := json.NewEncoder(st).Encode(config); err != nil {
|
||||
beforeFail()
|
||||
fmsg.Fatalf("cannot send configuration: %v", err)
|
||||
panic("unreachable")
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
beforeFail()
|
||||
fmsg.Fatalf("cannot start fortify: %v", err)
|
||||
panic("unreachable")
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
beforeFail()
|
||||
fmsg.Exit(exitError.ExitCode())
|
||||
panic("unreachable")
|
||||
} else {
|
||||
beforeFail()
|
||||
fmsg.Fatalf("cannot wait: %v", err)
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"path"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func actionStart(args []string) {
|
||||
set := flag.NewFlagSet("start", flag.ExitOnError)
|
||||
var (
|
||||
dropShell bool
|
||||
dropShellNixGL bool
|
||||
autoDrivers bool
|
||||
)
|
||||
set.BoolVar(&dropShell, "s", false, "Drop to a shell")
|
||||
set.BoolVar(&dropShellNixGL, "sg", false, "Drop to a shell on nixGL build")
|
||||
set.BoolVar(&autoDrivers, "autodrivers", false, "Attempt automatic opengl driver detection")
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args)
|
||||
|
||||
args = set.Args()
|
||||
|
||||
if len(args) < 1 {
|
||||
fmsg.Fatal("invalid argument")
|
||||
}
|
||||
|
||||
/*
|
||||
Parse app metadata.
|
||||
*/
|
||||
|
||||
id := args[0]
|
||||
pathSet := pathSetByApp(id)
|
||||
app := loadBundleInfo(pathSet.metaPath, func() {})
|
||||
if app.ID != id {
|
||||
fmsg.Fatalf("app %q claims to have identifier %q", id, app.ID)
|
||||
}
|
||||
|
||||
/*
|
||||
Prepare nixGL.
|
||||
*/
|
||||
|
||||
if app.GPU && autoDrivers {
|
||||
withNixDaemon("nix-gl", []string{
|
||||
"mkdir -p /nix/.nixGL/auto",
|
||||
"rm -rf /nix/.nixGL/auto",
|
||||
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/opengl " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + app.NixGL,
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + app.NixGL + "#nixVulkanNvidia",
|
||||
}, true, func(config *fst.Config) *fst.Config {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||
{Src: "/etc/resolv.conf"},
|
||||
{Src: "/sys/block"},
|
||||
{Src: "/sys/bus"},
|
||||
{Src: "/sys/class"},
|
||||
{Src: "/sys/dev"},
|
||||
{Src: "/sys/devices"},
|
||||
}...)
|
||||
appendGPUFilesystem(config)
|
||||
return config
|
||||
}, app, pathSet, dropShellNixGL, func() {})
|
||||
}
|
||||
|
||||
/*
|
||||
Create app configuration.
|
||||
*/
|
||||
|
||||
command := make([]string, 1, len(args))
|
||||
if !dropShell {
|
||||
command[0] = app.Launcher
|
||||
} else {
|
||||
command[0] = shell
|
||||
}
|
||||
command = append(command, args[1:]...)
|
||||
|
||||
config := &fst.Config{
|
||||
ID: app.ID,
|
||||
Command: command,
|
||||
Confinement: fst.ConfinementConfig{
|
||||
AppID: app.AppID,
|
||||
Groups: app.Groups,
|
||||
Username: "fortify",
|
||||
Inner: path.Join("/data/data", app.ID),
|
||||
Outer: pathSet.homeDir,
|
||||
Sandbox: &fst.SandboxConfig{
|
||||
Hostname: formatHostname(app.Name),
|
||||
UserNS: app.UserNS,
|
||||
Net: app.Net,
|
||||
Dev: app.Dev,
|
||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
|
||||
NoNewSession: app.NoNewSession || dropShell,
|
||||
MapRealUID: app.MapRealUID,
|
||||
DirectWayland: app.DirectWayland,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
|
||||
{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
|
||||
{Src: "/etc/resolv.conf"},
|
||||
{Src: "/sys/block"},
|
||||
{Src: "/sys/bus"},
|
||||
{Src: "/sys/class"},
|
||||
{Src: "/sys/dev"},
|
||||
{Src: "/sys/devices"},
|
||||
},
|
||||
Link: [][2]string{
|
||||
{app.CurrentSystem, "/run/current-system"},
|
||||
{"/run/current-system/sw/bin", "/bin"},
|
||||
{"/run/current-system/sw/bin", "/usr/bin"},
|
||||
},
|
||||
Etc: path.Join(pathSet.cacheDir, "etc"),
|
||||
AutoEtc: true,
|
||||
},
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
SystemBus: app.SystemBus,
|
||||
SessionBus: app.SessionBus,
|
||||
Enablements: app.Enablements,
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Expose GPU devices.
|
||||
*/
|
||||
|
||||
if app.GPU {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
|
||||
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
|
||||
appendGPUFilesystem(config)
|
||||
}
|
||||
|
||||
/*
|
||||
Spawn app.
|
||||
*/
|
||||
|
||||
fortifyApp(config, func() {})
|
||||
fmsg.Exit(0)
|
||||
}
|
||||
|
||||
func appendGPUFilesystem(config *fst.Config) {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
|
||||
{Src: "/dev/dri", Device: true},
|
||||
// mali
|
||||
{Src: "/dev/mali", Device: true},
|
||||
{Src: "/dev/mali0", Device: true},
|
||||
{Src: "/dev/umplock", Device: true},
|
||||
// nvidia
|
||||
{Src: "/dev/nvidiactl", Device: true},
|
||||
{Src: "/dev/nvidia-modeset", Device: true},
|
||||
// nvidia OpenCL/CUDA
|
||||
{Src: "/dev/nvidia-uvm", Device: true},
|
||||
{Src: "/dev/nvidia-uvm-tools", Device: true},
|
||||
|
||||
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
|
||||
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
|
||||
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
|
||||
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
|
||||
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
|
||||
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
|
||||
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
|
||||
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
|
||||
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
|
||||
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
|
||||
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
|
||||
}...)
|
||||
}
|
||||
101
cmd/fpkg/with.go
101
cmd/fpkg/with.go
@@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func withNixDaemon(
|
||||
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
|
||||
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||
) {
|
||||
fortifyAppDropShell(updateConfig(&fst.Config{
|
||||
ID: app.ID,
|
||||
Command: []string{shell, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
|
||||
// start nix-daemon
|
||||
"nix-daemon --store / & " +
|
||||
// wait for socket to appear
|
||||
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
|
||||
// create directory so nix stops complaining
|
||||
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
|
||||
strings.Join(command, " && ") +
|
||||
// terminate nix-daemon
|
||||
" && pkill nix-daemon",
|
||||
},
|
||||
Confinement: fst.ConfinementConfig{
|
||||
AppID: app.AppID,
|
||||
Username: "fortify",
|
||||
Inner: path.Join("/data/data", app.ID),
|
||||
Outer: pathSet.homeDir,
|
||||
Sandbox: &fst.SandboxConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
UserNS: true, // nix sandbox requires userns
|
||||
Net: net,
|
||||
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
|
||||
NoNewSession: dropShell,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
|
||||
},
|
||||
Link: [][2]string{
|
||||
{app.CurrentSystem, "/run/current-system"},
|
||||
{"/run/current-system/sw/bin", "/bin"},
|
||||
{"/run/current-system/sw/bin", "/usr/bin"},
|
||||
},
|
||||
Etc: path.Join(pathSet.cacheDir, "etc"),
|
||||
AutoEtc: true,
|
||||
},
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
},
|
||||
}), dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
fortifyAppDropShell(&fst.Config{
|
||||
ID: app.ID,
|
||||
Command: []string{shell, "-lc", strings.Join(command, " && ")},
|
||||
Confinement: fst.ConfinementConfig{
|
||||
AppID: app.AppID,
|
||||
Username: "nixos",
|
||||
Inner: path.Join("/data/data", app.ID, "cache"),
|
||||
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim
|
||||
Sandbox: &fst.SandboxConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
|
||||
NoNewSession: dropShell,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
|
||||
{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
|
||||
},
|
||||
Link: [][2]string{
|
||||
{app.CurrentSystem, "/run/current-system"},
|
||||
{"/run/current-system/sw/bin", "/bin"},
|
||||
{"/run/current-system/sw/bin", "/usr/bin"},
|
||||
},
|
||||
Etc: path.Join(workDir, "etc"),
|
||||
AutoEtc: true,
|
||||
},
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
{Path: workDir, Execute: true},
|
||||
},
|
||||
},
|
||||
}, dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
|
||||
if dropShell {
|
||||
config.Command = []string{shell, "-l"}
|
||||
fortifyApp(config, beforeFail)
|
||||
beforeFail()
|
||||
fmsg.Exit(0)
|
||||
}
|
||||
fortifyApp(config, beforeFail)
|
||||
}
|
||||
266
cmd/hakurei/command.go
Normal file
266
cmd/hakurei/command.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/app"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func buildCommand(out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
|
||||
|
||||
c.Command("app", "Load app from configuration file", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("app requires at least 1 argument")
|
||||
}
|
||||
|
||||
// config extraArgs...
|
||||
config := tryPath(args[0])
|
||||
config.Args = append(config.Args, args[1:]...)
|
||||
|
||||
runApp(config)
|
||||
panic("unreachable")
|
||||
})
|
||||
|
||||
{
|
||||
var (
|
||||
dbusConfigSession string
|
||||
dbusConfigSystem string
|
||||
mpris bool
|
||||
dbusVerbose bool
|
||||
|
||||
fid string
|
||||
aid int
|
||||
groups command.RepeatableFlag
|
||||
homeDir string
|
||||
userName string
|
||||
|
||||
wayland, x11, dBus, pulse bool
|
||||
)
|
||||
|
||||
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||
// initialise config from flags
|
||||
config := &hst.Config{
|
||||
ID: fid,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
if aid < 0 || aid > 9999 {
|
||||
log.Fatalf("aid %d out of range", aid)
|
||||
}
|
||||
|
||||
// resolve home/username from os when flag is unset
|
||||
var (
|
||||
passwd *user.User
|
||||
passwdOnce sync.Once
|
||||
passwdFunc = func() {
|
||||
var us string
|
||||
if uid, err := std.Uid(aid); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
us = strconv.Itoa(uid)
|
||||
}
|
||||
|
||||
if u, err := user.LookupId(us); err != nil {
|
||||
hlog.Verbosef("cannot look up uid %s", us)
|
||||
passwd = &user.User{
|
||||
Uid: us,
|
||||
Gid: us,
|
||||
Username: "chronos",
|
||||
Name: "Hakurei Permissive Default",
|
||||
HomeDir: container.FHSVarEmpty,
|
||||
}
|
||||
} else {
|
||||
passwd = u
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if homeDir == "os" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
homeDir = passwd.HomeDir
|
||||
}
|
||||
|
||||
if userName == "chronos" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
userName = passwd.Username
|
||||
}
|
||||
|
||||
config.Identity = aid
|
||||
config.Groups = groups
|
||||
config.Username = userName
|
||||
|
||||
if a, err := container.NewAbs(homeDir); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return err
|
||||
} else {
|
||||
config.Home = a
|
||||
}
|
||||
|
||||
var e system.Enablement
|
||||
if wayland {
|
||||
e |= system.EWayland
|
||||
}
|
||||
if x11 {
|
||||
e |= system.EX11
|
||||
}
|
||||
if dBus {
|
||||
e |= system.EDBus
|
||||
}
|
||||
if pulse {
|
||||
e |= system.EPulse
|
||||
}
|
||||
config.Enablements = hst.NewEnablements(e)
|
||||
|
||||
// parse D-Bus config file from flags if applicable
|
||||
if dBus {
|
||||
if dbusConfigSession == "builtin" {
|
||||
config.SessionBus = dbus.NewConfig(fid, true, mpris)
|
||||
} else {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
||||
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
||||
} else {
|
||||
config.SessionBus = conf
|
||||
}
|
||||
}
|
||||
|
||||
// system bus proxy is optional
|
||||
if dbusConfigSystem != "nil" {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
||||
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
||||
} else {
|
||||
config.SystemBus = conf
|
||||
}
|
||||
}
|
||||
|
||||
// override log from configuration
|
||||
if dbusVerbose {
|
||||
config.SessionBus.Log = true
|
||||
config.SystemBus.Log = true
|
||||
}
|
||||
}
|
||||
|
||||
// invoke app
|
||||
runApp(config)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
||||
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||
"Path to system bus proxy config file, or \"nil\" to disable").
|
||||
Flag(&mpris, "mpris", command.BoolFlag(false),
|
||||
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
||||
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
|
||||
"Force buffered logging in the D-Bus proxy").
|
||||
Flag(&fid, "id", command.StringFlag(""),
|
||||
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
||||
Flag(&aid, "a", command.IntFlag(0),
|
||||
"Application identity").
|
||||
Flag(nil, "g", &groups,
|
||||
"Groups inherited by all container processes").
|
||||
Flag(&homeDir, "d", command.StringFlag("os"),
|
||||
"Container home directory").
|
||||
Flag(&userName, "u", command.StringFlag("chronos"),
|
||||
"Passwd user name within sandbox").
|
||||
Flag(&wayland, "wayland", command.BoolFlag(false),
|
||||
"Enable connection to Wayland via security-context-v1").
|
||||
Flag(&x11, "X", command.BoolFlag(false),
|
||||
"Enable direct connection to X11").
|
||||
Flag(&dBus, "dbus", command.BoolFlag(false),
|
||||
"Enable proxied connection to D-Bus").
|
||||
Flag(&pulse, "pulse", command.BoolFlag(false),
|
||||
"Enable direct connection to PulseAudio")
|
||||
}
|
||||
|
||||
var showFlagShort bool
|
||||
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, showFlagShort, flagJSON)
|
||||
|
||||
case 1: // instance
|
||||
name := args[0]
|
||||
config, entry := tryShort(name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
}
|
||||
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
|
||||
|
||||
default:
|
||||
log.Fatal("show requires 1 argument")
|
||||
}
|
||||
return errSuccess
|
||||
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||
|
||||
var psFlagShort bool
|
||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
|
||||
c.Command("version", "Display version information", func(args []string) error {
|
||||
fmt.Println(internal.Version())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("license", "Show full license text", func(args []string) error {
|
||||
fmt.Println(license)
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("template", "Produce a config template", func(args []string) error {
|
||||
printJSON(os.Stdout, false, hst.Template())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("help", "Show this help message", func([]string) error {
|
||||
c.PrintHelp()
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func runApp(config *hst.Config) {
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
a := app.MustNew(ctx, std)
|
||||
|
||||
rs := new(app.RunState)
|
||||
if sa, err := a.Seal(config); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot seal app:")
|
||||
internal.Exit(1)
|
||||
} else {
|
||||
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
|
||||
}
|
||||
|
||||
*(*int)(nil) = 0 // not reached
|
||||
}
|
||||
81
cmd/hakurei/command_test.go
Normal file
81
cmd/hakurei/command_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"main", []string{}, `
|
||||
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
app Load app from configuration file
|
||||
run Configure and start a permissive default sandbox
|
||||
show Show live or local app configuration
|
||||
ps List active instances
|
||||
version Display version information
|
||||
license Show full license text
|
||||
template Produce a config template
|
||||
help Show this help message
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
"run", []string{"run", "-h"}, `
|
||||
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
||||
|
||||
Flags:
|
||||
-X Enable direct connection to X11
|
||||
-a int
|
||||
Application identity
|
||||
-d string
|
||||
Container home directory (default "os")
|
||||
-dbus
|
||||
Enable proxied connection to D-Bus
|
||||
-dbus-config string
|
||||
Path to session bus proxy config file, or "builtin" for defaults (default "builtin")
|
||||
-dbus-log
|
||||
Force buffered logging in the D-Bus proxy
|
||||
-dbus-system string
|
||||
Path to system bus proxy config file, or "nil" to disable (default "nil")
|
||||
-g value
|
||||
Groups inherited by all container processes
|
||||
-id string
|
||||
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||
-mpris
|
||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||
-pulse
|
||||
Enable direct connection to PulseAudio
|
||||
-u string
|
||||
Passwd user name within sandbox (default "chronos")
|
||||
-wayland
|
||||
Enable connection to Wayland via security-context-v1
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := new(bytes.Buffer)
|
||||
c := buildCommand(out)
|
||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||
t.Errorf("Parse: error = %v; want %v",
|
||||
err, command.ErrHelp)
|
||||
}
|
||||
if got := out.String(); got != tc.want {
|
||||
t.Errorf("Parse: %s want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
56
cmd/hakurei/main.go
Normal file
56
cmd/hakurei/main.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
// this works around go:embed '..' limitation
|
||||
//go:generate cp ../../LICENSE .
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
|
||||
//go:embed LICENSE
|
||||
license string
|
||||
)
|
||||
|
||||
func init() { hlog.Prepare("hakurei") }
|
||||
|
||||
var std sys.State = new(sys.Std)
|
||||
|
||||
func main() {
|
||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||
|
||||
if err := container.SetPtracer(0); err != nil {
|
||||
hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
|
||||
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
// this catches faulty command handlers that fail to return before this point
|
||||
})
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
@@ -4,28 +4,28 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
func tryPath(name string) (config *fst.Config) {
|
||||
func tryPath(name string) (config *hst.Config) {
|
||||
var r io.Reader
|
||||
config = new(fst.Config)
|
||||
config = new(hst.Config)
|
||||
|
||||
if name != "-" {
|
||||
r = tryFd(name)
|
||||
if r == nil {
|
||||
fmsg.VPrintln("load configuration from file")
|
||||
hlog.Verbose("load configuration from file")
|
||||
|
||||
if f, err := os.Open(name); err != nil {
|
||||
fmsg.Fatalf("cannot access configuration file %q: %s", name, err)
|
||||
panic("unreachable")
|
||||
log.Fatalf("cannot access configuration file %q: %s", name, err)
|
||||
} else {
|
||||
// finalizer closes f
|
||||
r = f
|
||||
@@ -33,7 +33,7 @@ func tryPath(name string) (config *fst.Config) {
|
||||
} else {
|
||||
defer func() {
|
||||
if err := r.(io.ReadCloser).Close(); err != nil {
|
||||
fmsg.Printf("cannot close config fd: %v", err)
|
||||
log.Printf("cannot close config fd: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -42,8 +42,7 @@ func tryPath(name string) (config *fst.Config) {
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r).Decode(&config); err != nil {
|
||||
fmsg.Fatalf("cannot load configuration: %v", err)
|
||||
panic("unreachable")
|
||||
log.Fatalf("cannot load configuration: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -51,21 +50,24 @@ func tryPath(name string) (config *fst.Config) {
|
||||
|
||||
func tryFd(name string) io.ReadCloser {
|
||||
if v, err := strconv.Atoi(name); err != nil {
|
||||
fmsg.VPrintf("name cannot be interpreted as int64: %v", err)
|
||||
if !errors.Is(err, strconv.ErrSyntax) {
|
||||
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
hlog.Verbosef("trying config stream from %d", v)
|
||||
fd := uintptr(v)
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
|
||||
if errors.Is(errno, syscall.EBADF) {
|
||||
return nil
|
||||
}
|
||||
fmsg.Fatalf("cannot get fd %d: %v", fd, errno)
|
||||
log.Fatalf("cannot get fd %d: %v", fd, errno)
|
||||
}
|
||||
return os.NewFile(fd, strconv.Itoa(v))
|
||||
}
|
||||
}
|
||||
|
||||
func tryShort(name string) (config *fst.Config, instance *state.State) {
|
||||
func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||
likePrefix := false
|
||||
if len(name) <= 32 {
|
||||
likePrefix = true
|
||||
@@ -83,23 +85,23 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
|
||||
|
||||
// try to match from state store
|
||||
if likePrefix && len(name) >= 8 {
|
||||
fmsg.VPrintln("argument looks like prefix")
|
||||
hlog.Verbose("argument looks like prefix")
|
||||
|
||||
s := state.NewMulti(sys.Paths().RunDirPath)
|
||||
s := state.NewMulti(std.Paths().RunDirPath.String())
|
||||
if entries, err := state.Join(s); err != nil {
|
||||
fmsg.Printf("cannot join store: %v", err)
|
||||
log.Printf("cannot join store: %v", err)
|
||||
// drop to fetch from file
|
||||
} else {
|
||||
for id := range entries {
|
||||
v := id.String()
|
||||
if strings.HasPrefix(v, name) {
|
||||
// match, use config from this state entry
|
||||
instance = entries[id]
|
||||
config = instance.Config
|
||||
entry = entries[id]
|
||||
config = entry.Config
|
||||
break
|
||||
}
|
||||
|
||||
fmsg.VPrintf("instance %s skipped", v)
|
||||
hlog.Verbosef("instance %s skipped", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,27 +4,30 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func printShowSystem(output io.Writer, short bool) {
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
info := new(fst.Info)
|
||||
info := &hst.Info{Paths: std.Paths()}
|
||||
|
||||
// get fid by querying uid of aid 0
|
||||
if uid, err := sys.Uid(0); err != nil {
|
||||
fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
|
||||
// get hid by querying uid of identity 0
|
||||
if uid, err := std.Uid(0); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
info.User = (uid / 10000) - 100
|
||||
}
|
||||
@@ -35,12 +38,16 @@ func printShowSystem(output io.Writer, short bool) {
|
||||
}
|
||||
|
||||
t.Printf("User:\t%d\n", info.User)
|
||||
t.Printf("TempDir:\t%s\n", info.TempDir)
|
||||
t.Printf("SharePath:\t%s\n", info.SharePath)
|
||||
t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
|
||||
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
|
||||
}
|
||||
|
||||
func printShowInstance(
|
||||
output io.Writer, now time.Time,
|
||||
instance *state.State, config *fst.Config,
|
||||
short bool) {
|
||||
instance *state.State, config *hst.Config,
|
||||
short, flagJSON bool) {
|
||||
if flagJSON {
|
||||
if instance != nil {
|
||||
printJSON(output, short, instance)
|
||||
@@ -53,7 +60,7 @@ func printShowInstance(
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
if config.Confinement.Sandbox == nil {
|
||||
if config.Container == nil {
|
||||
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
|
||||
}
|
||||
|
||||
@@ -66,19 +73,21 @@ func printShowInstance(
|
||||
|
||||
t.Printf("App\n")
|
||||
if config.ID != "" {
|
||||
t.Printf(" ID:\t%d (%s)\n", config.Confinement.AppID, config.ID)
|
||||
t.Printf(" Identity:\t%d (%s)\n", config.Identity, config.ID)
|
||||
} else {
|
||||
t.Printf(" ID:\t%d\n", config.Confinement.AppID)
|
||||
t.Printf(" Identity:\t%d\n", config.Identity)
|
||||
}
|
||||
t.Printf(" Enablements:\t%s\n", config.Confinement.Enablements.String())
|
||||
if len(config.Confinement.Groups) > 0 {
|
||||
t.Printf(" Groups:\t%q\n", config.Confinement.Groups)
|
||||
t.Printf(" Enablements:\t%s\n", config.Enablements.Unwrap().String())
|
||||
if len(config.Groups) > 0 {
|
||||
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
||||
}
|
||||
t.Printf(" Directory:\t%s\n", config.Confinement.Outer)
|
||||
if config.Confinement.Sandbox != nil {
|
||||
sandbox := config.Confinement.Sandbox
|
||||
if sandbox.Hostname != "" {
|
||||
t.Printf(" Hostname:\t%q\n", sandbox.Hostname)
|
||||
if config.Home != nil {
|
||||
t.Printf(" Home:\t%s\n", config.Home)
|
||||
}
|
||||
if config.Container != nil {
|
||||
params := config.Container
|
||||
if params.Hostname != "" {
|
||||
t.Printf(" Hostname:\t%s\n", params.Hostname)
|
||||
}
|
||||
flags := make([]string, 0, 7)
|
||||
writeFlag := func(name string, value bool) {
|
||||
@@ -86,68 +95,43 @@ func printShowInstance(
|
||||
flags = append(flags, name)
|
||||
}
|
||||
}
|
||||
writeFlag("userns", sandbox.UserNS)
|
||||
writeFlag("net", sandbox.Net)
|
||||
writeFlag("dev", sandbox.Dev)
|
||||
writeFlag("tty", sandbox.NoNewSession)
|
||||
writeFlag("mapuid", sandbox.MapRealUID)
|
||||
writeFlag("directwl", sandbox.DirectWayland)
|
||||
writeFlag("autoetc", sandbox.AutoEtc)
|
||||
writeFlag("userns", params.Userns)
|
||||
writeFlag("devel", params.Devel)
|
||||
writeFlag("net", params.HostNet)
|
||||
writeFlag("abstract", params.HostAbstract)
|
||||
writeFlag("device", params.Device)
|
||||
writeFlag("tty", params.Tty)
|
||||
writeFlag("mapuid", params.MapRealUID)
|
||||
writeFlag("directwl", config.DirectWayland)
|
||||
if len(flags) == 0 {
|
||||
flags = append(flags, "none")
|
||||
}
|
||||
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
|
||||
|
||||
etc := sandbox.Etc
|
||||
if etc == "" {
|
||||
etc = "/etc"
|
||||
if config.Path != nil {
|
||||
t.Printf(" Path:\t%s\n", config.Path)
|
||||
}
|
||||
t.Printf(" Etc:\t%s\n", etc)
|
||||
|
||||
if len(sandbox.Override) > 0 {
|
||||
t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
|
||||
}
|
||||
|
||||
// Env map[string]string `json:"env"`
|
||||
// Link [][2]string `json:"symlink"`
|
||||
if len(config.Args) > 0 {
|
||||
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
|
||||
}
|
||||
t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
|
||||
t.Printf("\n")
|
||||
|
||||
if !short {
|
||||
if config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 {
|
||||
if config.Container != nil && len(config.Container.Filesystem) > 0 {
|
||||
t.Printf("Filesystem\n")
|
||||
for _, f := range config.Confinement.Sandbox.Filesystem {
|
||||
if f == nil {
|
||||
for _, f := range config.Container.Filesystem {
|
||||
if !f.Valid() {
|
||||
t.Println(" <invalid>")
|
||||
continue
|
||||
}
|
||||
|
||||
expr := new(strings.Builder)
|
||||
expr.Grow(3 + len(f.Src) + 1 + len(f.Dst))
|
||||
|
||||
if f.Device {
|
||||
expr.WriteString(" d")
|
||||
} else if f.Write {
|
||||
expr.WriteString(" w")
|
||||
} else {
|
||||
expr.WriteString(" ")
|
||||
}
|
||||
if f.Must {
|
||||
expr.WriteString("*")
|
||||
} else {
|
||||
expr.WriteString("+")
|
||||
}
|
||||
expr.WriteString(f.Src)
|
||||
if f.Dst != "" {
|
||||
expr.WriteString(":" + f.Dst)
|
||||
}
|
||||
t.Printf("%s\n", expr.String())
|
||||
t.Printf(" %s\n", f)
|
||||
}
|
||||
t.Printf("\n")
|
||||
}
|
||||
if len(config.Confinement.ExtraPerms) > 0 {
|
||||
if len(config.ExtraPerms) > 0 {
|
||||
t.Printf("Extra ACL\n")
|
||||
for _, p := range config.Confinement.ExtraPerms {
|
||||
for _, p := range config.ExtraPerms {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
@@ -175,27 +159,27 @@ func printShowInstance(
|
||||
t.Printf(" Broadcast:\t%q\n", c.Broadcast)
|
||||
}
|
||||
}
|
||||
if config.Confinement.SessionBus != nil {
|
||||
if config.SessionBus != nil {
|
||||
t.Printf("Session bus\n")
|
||||
printDBus(config.Confinement.SessionBus)
|
||||
printDBus(config.SessionBus)
|
||||
t.Printf("\n")
|
||||
}
|
||||
if config.Confinement.SystemBus != nil {
|
||||
if config.SystemBus != nil {
|
||||
t.Printf("System bus\n")
|
||||
printDBus(config.Confinement.SystemBus)
|
||||
printDBus(config.SystemBus)
|
||||
t.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
|
||||
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
|
||||
var entries state.Entries
|
||||
if e, err := state.Join(s); err != nil {
|
||||
fmsg.Fatalf("cannot join store: %v", err)
|
||||
log.Fatalf("cannot join store: %v", err)
|
||||
} else {
|
||||
entries = e
|
||||
}
|
||||
if err := s.Close(); err != nil {
|
||||
fmsg.Printf("cannot close store: %v", err)
|
||||
log.Printf("cannot close store: %v", err)
|
||||
}
|
||||
|
||||
if !short && flagJSON {
|
||||
@@ -212,13 +196,13 @@ func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
|
||||
for id, instance := range entries {
|
||||
// gracefully skip nil states
|
||||
if instance == nil {
|
||||
fmsg.Printf("got invalid state entry %s", id.String())
|
||||
log.Printf("got invalid state entry %s", id.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// gracefully skip inconsistent states
|
||||
if id != instance.ID {
|
||||
fmsg.Printf("possible store corruption: entry %s has id %s",
|
||||
log.Printf("possible store corruption: entry %s has id %s",
|
||||
id.String(), instance.ID.String())
|
||||
continue
|
||||
}
|
||||
@@ -244,22 +228,26 @@ func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
|
||||
t.Println("\tInstance\tPID\tApplication\tUptime")
|
||||
for _, e := range exp {
|
||||
var (
|
||||
es = "(No confinement information)"
|
||||
cs = "(No command information)"
|
||||
as = "(No configuration information)"
|
||||
)
|
||||
if len(e.s) != 1<<5 {
|
||||
// unreachable
|
||||
log.Printf("possible store corruption: invalid instance string %s", e.s)
|
||||
continue
|
||||
}
|
||||
|
||||
as := "(No configuration information)"
|
||||
if e.Config != nil {
|
||||
es = e.Config.Confinement.Enablements.String()
|
||||
cs = fmt.Sprintf("%q", e.Config.Command)
|
||||
as = strconv.Itoa(e.Config.Confinement.AppID)
|
||||
as = strconv.Itoa(e.Config.Identity)
|
||||
id := e.Config.ID
|
||||
if id == "" {
|
||||
id = "app.hakurei." + e.s[:8]
|
||||
}
|
||||
t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
|
||||
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String(), strings.TrimPrefix(es, ", "), cs)
|
||||
as += " (" + id + ")"
|
||||
}
|
||||
t.Printf("\t%s\t%d\t%s\t%s\n",
|
||||
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String())
|
||||
}
|
||||
t.Println()
|
||||
}
|
||||
|
||||
type expandedStateEntry struct {
|
||||
@@ -273,8 +261,7 @@ func printJSON(output io.Writer, short bool, v any) {
|
||||
encoder.SetIndent("", " ")
|
||||
}
|
||||
if err := encoder.Encode(v); err != nil {
|
||||
fmsg.Fatalf("cannot serialise: %v", err)
|
||||
panic("unreachable")
|
||||
log.Fatalf("cannot serialise: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,31 +271,26 @@ type tp struct{ *tabwriter.Writer }
|
||||
|
||||
func (p *tp) Printf(format string, a ...any) {
|
||||
if _, err := fmt.Fprintf(p, format, a...); err != nil {
|
||||
fmsg.Fatalf("cannot write to tabwriter: %v", err)
|
||||
panic("unreachable")
|
||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
func (p *tp) Println(a ...any) {
|
||||
if _, err := fmt.Fprintln(p, a...); err != nil {
|
||||
fmsg.Fatalf("cannot write to tabwriter: %v", err)
|
||||
panic("unreachable")
|
||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
func (p *tp) MustFlush() {
|
||||
if err := p.Writer.Flush(); err != nil {
|
||||
fmsg.Fatalf("cannot flush tabwriter: %v", err)
|
||||
panic("unreachable")
|
||||
log.Fatalf("cannot flush tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
func mustPrint(output io.Writer, a ...any) {
|
||||
if _, err := fmt.Fprint(output, a...); err != nil {
|
||||
fmsg.Fatalf("cannot print: %v", err)
|
||||
panic("unreachable")
|
||||
log.Fatalf("cannot print: %v", err)
|
||||
}
|
||||
}
|
||||
func mustPrintln(output io.Writer, a ...any) {
|
||||
if _, err := fmt.Fprintln(output, a...); err != nil {
|
||||
fmsg.Fatalf("cannot print: %v", err)
|
||||
panic("unreachable")
|
||||
log.Fatalf("cannot print: %v", err)
|
||||
}
|
||||
}
|
||||
732
cmd/hakurei/print_test.go
Normal file
732
cmd/hakurei/print_test.go
Normal file
@@ -0,0 +1,732 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
var (
|
||||
testID = state.ID{
|
||||
0x8e, 0x2c, 0x76, 0xb0,
|
||||
0x66, 0xda, 0xbe, 0x57,
|
||||
0x4c, 0xf0, 0x73, 0xbd,
|
||||
0xb4, 0x6e, 0xb5, 0xc1,
|
||||
}
|
||||
testState = &state.State{
|
||||
ID: testID,
|
||||
PID: 0xDEADBEEF,
|
||||
Config: hst.Template(),
|
||||
Time: testAppTime,
|
||||
}
|
||||
testTime = time.Unix(3752, 1).UTC()
|
||||
testAppTime = time.Unix(0, 9).UTC()
|
||||
)
|
||||
|
||||
func Test_printShowInstance(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
instance *state.State
|
||||
config *hst.Config
|
||||
short, json bool
|
||||
want string
|
||||
}{
|
||||
{"config", nil, hst.Template(), false, false, `App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Home: /data/data/org.chromium.Chromium
|
||||
Hostname: localhost
|
||||
Flags: userns devel net abstract device tty mapuid
|
||||
Path: /run/current-system/sw/bin/chromium
|
||||
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||
|
||||
Filesystem
|
||||
autoroot:w:/var/lib/hakurei/base/org.debian
|
||||
autoetc:/etc/
|
||||
w+ephemeral(-rwxr-xr-x):/tmp/
|
||||
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
|
||||
*/nix/store
|
||||
/run/current-system@
|
||||
/run/opengl-driver@
|
||||
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
d+/dev/dri
|
||||
|
||||
Extra ACL
|
||||
--x+:/var/lib/hakurei/u0
|
||||
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
|
||||
|
||||
Session bus
|
||||
Filter: true
|
||||
Talk: ["org.freedesktop.Notifications" "org.freedesktop.FileManager1" "org.freedesktop.ScreenSaver" "org.freedesktop.secrets" "org.kde.kwalletd5" "org.kde.kwalletd6" "org.gnome.SessionManager"]
|
||||
Own: ["org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.chromium.*"]
|
||||
Call: map["org.freedesktop.portal.*":"*"]
|
||||
Broadcast: map["org.freedesktop.portal.*":"@/org/freedesktop/portal/*"]
|
||||
|
||||
System bus
|
||||
Filter: true
|
||||
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
|
||||
|
||||
`},
|
||||
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
`},
|
||||
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
Flags: none
|
||||
|
||||
`},
|
||||
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
Flags: none
|
||||
|
||||
Filesystem
|
||||
<invalid>
|
||||
|
||||
Extra ACL
|
||||
|
||||
`},
|
||||
{"config pd dbus see", nil, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
Session bus
|
||||
Filter: false
|
||||
See: ["org.example.test"]
|
||||
|
||||
`},
|
||||
|
||||
{"instance", testState, hst.Template(), false, false, `State
|
||||
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
|
||||
Uptime: 1h2m32s
|
||||
|
||||
App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Home: /data/data/org.chromium.Chromium
|
||||
Hostname: localhost
|
||||
Flags: userns devel net abstract device tty mapuid
|
||||
Path: /run/current-system/sw/bin/chromium
|
||||
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||
|
||||
Filesystem
|
||||
autoroot:w:/var/lib/hakurei/base/org.debian
|
||||
autoetc:/etc/
|
||||
w+ephemeral(-rwxr-xr-x):/tmp/
|
||||
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
|
||||
*/nix/store
|
||||
/run/current-system@
|
||||
/run/opengl-driver@
|
||||
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
d+/dev/dri
|
||||
|
||||
Extra ACL
|
||||
--x+:/var/lib/hakurei/u0
|
||||
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
|
||||
|
||||
Session bus
|
||||
Filter: true
|
||||
Talk: ["org.freedesktop.Notifications" "org.freedesktop.FileManager1" "org.freedesktop.ScreenSaver" "org.freedesktop.secrets" "org.kde.kwalletd5" "org.kde.kwalletd6" "org.gnome.SessionManager"]
|
||||
Own: ["org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.chromium.*"]
|
||||
Call: map["org.freedesktop.portal.*":"*"]
|
||||
Broadcast: map["org.freedesktop.portal.*":"@/org/freedesktop/portal/*"]
|
||||
|
||||
System bus
|
||||
Filter: true
|
||||
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
|
||||
|
||||
`},
|
||||
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
State
|
||||
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
|
||||
Uptime: 1h2m32s
|
||||
|
||||
App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
`},
|
||||
|
||||
{"json nil", nil, nil, false, true, `null
|
||||
`},
|
||||
{"json instance", testState, nil, false, true, `{
|
||||
"instance": [
|
||||
142,
|
||||
44,
|
||||
118,
|
||||
176,
|
||||
102,
|
||||
218,
|
||||
190,
|
||||
87,
|
||||
76,
|
||||
240,
|
||||
115,
|
||||
189,
|
||||
180,
|
||||
110,
|
||||
181,
|
||||
193
|
||||
],
|
||||
"pid": 3735928559,
|
||||
"config": {
|
||||
"id": "org.chromium.Chromium",
|
||||
"path": "/run/current-system/sw/bin/chromium",
|
||||
"args": [
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland"
|
||||
],
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pulse": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager"
|
||||
],
|
||||
"own": [
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*"
|
||||
],
|
||||
"call": {
|
||||
"org.freedesktop.portal.*": "*"
|
||||
},
|
||||
"broadcast": {
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
|
||||
},
|
||||
"filter": true
|
||||
},
|
||||
"system_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower"
|
||||
],
|
||||
"own": null,
|
||||
"call": null,
|
||||
"broadcast": null,
|
||||
"filter": true
|
||||
},
|
||||
"username": "chronos",
|
||||
"shell": "/run/current-system/sw/bin/zsh",
|
||||
"home": "/data/data/org.chromium.Chromium",
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/hakurei/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
}
|
||||
],
|
||||
"identity": 9,
|
||||
"groups": [
|
||||
"video",
|
||||
"dialout",
|
||||
"plugdev"
|
||||
],
|
||||
"container": {
|
||||
"hostname": "localhost",
|
||||
"wait_delay": -1,
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"seccomp_compat": true,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"host_net": true,
|
||||
"host_abstract": true,
|
||||
"tty": true,
|
||||
"multiarch": true,
|
||||
"env": {
|
||||
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
|
||||
},
|
||||
"map_real_uid": true,
|
||||
"device": true,
|
||||
"filesystem": [
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/",
|
||||
"src": "/var/lib/hakurei/base/org.debian",
|
||||
"write": true,
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/etc/",
|
||||
"src": "/etc/",
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"type": "ephemeral",
|
||||
"dst": "/tmp/",
|
||||
"write": true,
|
||||
"perm": 493
|
||||
},
|
||||
{
|
||||
"type": "overlay",
|
||||
"dst": "/nix/store",
|
||||
"lower": [
|
||||
"/mnt-root/nix/.ro-store"
|
||||
],
|
||||
"upper": "/mnt-root/nix/.rw-store/upper",
|
||||
"work": "/mnt-root/nix/.rw-store/work"
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"dst": "/run/current-system",
|
||||
"linkname": "/run/current-system",
|
||||
"dereference": true
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"dst": "/run/opengl-driver",
|
||||
"linkname": "/run/opengl-driver",
|
||||
"dereference": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"ensure": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"src": "/dev/dri",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"time": "1970-01-01T00:00:00.000000009Z"
|
||||
}
|
||||
`},
|
||||
{"json config", nil, hst.Template(), false, true, `{
|
||||
"id": "org.chromium.Chromium",
|
||||
"path": "/run/current-system/sw/bin/chromium",
|
||||
"args": [
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland"
|
||||
],
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pulse": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager"
|
||||
],
|
||||
"own": [
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*"
|
||||
],
|
||||
"call": {
|
||||
"org.freedesktop.portal.*": "*"
|
||||
},
|
||||
"broadcast": {
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
|
||||
},
|
||||
"filter": true
|
||||
},
|
||||
"system_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower"
|
||||
],
|
||||
"own": null,
|
||||
"call": null,
|
||||
"broadcast": null,
|
||||
"filter": true
|
||||
},
|
||||
"username": "chronos",
|
||||
"shell": "/run/current-system/sw/bin/zsh",
|
||||
"home": "/data/data/org.chromium.Chromium",
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/hakurei/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
}
|
||||
],
|
||||
"identity": 9,
|
||||
"groups": [
|
||||
"video",
|
||||
"dialout",
|
||||
"plugdev"
|
||||
],
|
||||
"container": {
|
||||
"hostname": "localhost",
|
||||
"wait_delay": -1,
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"seccomp_compat": true,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"host_net": true,
|
||||
"host_abstract": true,
|
||||
"tty": true,
|
||||
"multiarch": true,
|
||||
"env": {
|
||||
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
|
||||
},
|
||||
"map_real_uid": true,
|
||||
"device": true,
|
||||
"filesystem": [
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/",
|
||||
"src": "/var/lib/hakurei/base/org.debian",
|
||||
"write": true,
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/etc/",
|
||||
"src": "/etc/",
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"type": "ephemeral",
|
||||
"dst": "/tmp/",
|
||||
"write": true,
|
||||
"perm": 493
|
||||
},
|
||||
{
|
||||
"type": "overlay",
|
||||
"dst": "/nix/store",
|
||||
"lower": [
|
||||
"/mnt-root/nix/.ro-store"
|
||||
],
|
||||
"upper": "/mnt-root/nix/.rw-store/upper",
|
||||
"work": "/mnt-root/nix/.rw-store/work"
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"dst": "/run/current-system",
|
||||
"linkname": "/run/current-system",
|
||||
"dereference": true
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"dst": "/run/opengl-driver",
|
||||
"linkname": "/run/opengl-driver",
|
||||
"dereference": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"ensure": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"src": "/dev/dri",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
output := new(strings.Builder)
|
||||
printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
|
||||
if got := output.String(); got != tc.want {
|
||||
t.Errorf("printShowInstance: got\n%s\nwant\n%s",
|
||||
got, tc.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_printPs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
entries state.Entries
|
||||
short, json bool
|
||||
want string
|
||||
}{
|
||||
{"no entries", make(state.Entries), false, false, " Instance PID Application Uptime\n"},
|
||||
{"no entries short", make(state.Entries), true, false, ""},
|
||||
{"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
|
||||
{"state corruption", state.Entries{state.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
|
||||
|
||||
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
|
||||
8e2c76b0 256 0 (app.hakurei.8e2c76b0) 1h2m32s
|
||||
`},
|
||||
|
||||
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
|
||||
8e2c76b0 3735928559 9 (org.chromium.Chromium) 1h2m32s
|
||||
`},
|
||||
{"valid short", state.Entries{testID: testState}, true, false, "8e2c76b0\n"},
|
||||
{"valid json", state.Entries{testID: testState}, false, true, `{
|
||||
"8e2c76b066dabe574cf073bdb46eb5c1": {
|
||||
"instance": [
|
||||
142,
|
||||
44,
|
||||
118,
|
||||
176,
|
||||
102,
|
||||
218,
|
||||
190,
|
||||
87,
|
||||
76,
|
||||
240,
|
||||
115,
|
||||
189,
|
||||
180,
|
||||
110,
|
||||
181,
|
||||
193
|
||||
],
|
||||
"pid": 3735928559,
|
||||
"config": {
|
||||
"id": "org.chromium.Chromium",
|
||||
"path": "/run/current-system/sw/bin/chromium",
|
||||
"args": [
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland"
|
||||
],
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pulse": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager"
|
||||
],
|
||||
"own": [
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*"
|
||||
],
|
||||
"call": {
|
||||
"org.freedesktop.portal.*": "*"
|
||||
},
|
||||
"broadcast": {
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
|
||||
},
|
||||
"filter": true
|
||||
},
|
||||
"system_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower"
|
||||
],
|
||||
"own": null,
|
||||
"call": null,
|
||||
"broadcast": null,
|
||||
"filter": true
|
||||
},
|
||||
"username": "chronos",
|
||||
"shell": "/run/current-system/sw/bin/zsh",
|
||||
"home": "/data/data/org.chromium.Chromium",
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/hakurei/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
}
|
||||
],
|
||||
"identity": 9,
|
||||
"groups": [
|
||||
"video",
|
||||
"dialout",
|
||||
"plugdev"
|
||||
],
|
||||
"container": {
|
||||
"hostname": "localhost",
|
||||
"wait_delay": -1,
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"seccomp_compat": true,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"host_net": true,
|
||||
"host_abstract": true,
|
||||
"tty": true,
|
||||
"multiarch": true,
|
||||
"env": {
|
||||
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
|
||||
},
|
||||
"map_real_uid": true,
|
||||
"device": true,
|
||||
"filesystem": [
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/",
|
||||
"src": "/var/lib/hakurei/base/org.debian",
|
||||
"write": true,
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/etc/",
|
||||
"src": "/etc/",
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"type": "ephemeral",
|
||||
"dst": "/tmp/",
|
||||
"write": true,
|
||||
"perm": 493
|
||||
},
|
||||
{
|
||||
"type": "overlay",
|
||||
"dst": "/nix/store",
|
||||
"lower": [
|
||||
"/mnt-root/nix/.ro-store"
|
||||
],
|
||||
"upper": "/mnt-root/nix/.rw-store/upper",
|
||||
"work": "/mnt-root/nix/.rw-store/work"
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"dst": "/run/current-system",
|
||||
"linkname": "/run/current-system",
|
||||
"dereference": true
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"dst": "/run/opengl-driver",
|
||||
"linkname": "/run/opengl-driver",
|
||||
"dereference": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"ensure": true
|
||||
},
|
||||
{
|
||||
"type": "bind",
|
||||
"src": "/dev/dri",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"time": "1970-01-01T00:00:00.000000009Z"
|
||||
}
|
||||
}
|
||||
`},
|
||||
{"valid short json", state.Entries{testID: testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1"]
|
||||
`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
output := new(strings.Builder)
|
||||
printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json)
|
||||
if got := output.String(); got != tc.want {
|
||||
t.Errorf("printPs: got\n%s\nwant\n%s",
|
||||
got, tc.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// stubStore implements [state.Store] and returns test samples via [state.Joiner].
|
||||
type stubStore state.Entries
|
||||
|
||||
func (s stubStore) Join() (state.Entries, error) { return state.Entries(s), nil }
|
||||
func (s stubStore) Do(int, func(c state.Cursor)) (bool, error) { panic("unreachable") }
|
||||
func (s stubStore) List() ([]int, error) { panic("unreachable") }
|
||||
func (s stubStore) Close() error { return nil }
|
||||
7
cmd/hpkg/README
Normal file
7
cmd/hpkg/README
Normal file
@@ -0,0 +1,7 @@
|
||||
This program is a proof of concept and is now deprecated. It is only kept
|
||||
around for API demonstration purposes and to make the most out of the test
|
||||
suite.
|
||||
|
||||
This program is replaced by planterette, which can be found at
|
||||
https://git.gensokyo.uk/security/planterette. Development effort should be
|
||||
focused there instead.
|
||||
161
cmd/hpkg/app.go
Normal file
161
cmd/hpkg/app.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
type appInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
|
||||
// passed through to [hst.Config]
|
||||
ID string `json:"id"`
|
||||
// passed through to [hst.Config]
|
||||
Identity int `json:"identity"`
|
||||
// passed through to [hst.Config]
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Devel bool `json:"devel,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Userns bool `json:"userns,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
HostNet bool `json:"net,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
HostAbstract bool `json:"abstract,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Device bool `json:"dev,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Tty bool `json:"tty,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
MapRealUID bool `json:"map_real_uid,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Enablements *hst.Enablements `json:"enablements,omitempty"`
|
||||
|
||||
// passed through to [hst.Config]
|
||||
Multiarch bool `json:"multiarch,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Bluetooth bool `json:"bluetooth,omitempty"`
|
||||
|
||||
// allow gpu access within sandbox
|
||||
GPU bool `json:"gpu"`
|
||||
// store path to nixGL mesa wrappers
|
||||
Mesa string `json:"mesa,omitempty"`
|
||||
// store path to nixGL source
|
||||
NixGL string `json:"nix_gl,omitempty"`
|
||||
// store path to activate-and-exec script
|
||||
Launcher *container.Absolute `json:"launcher"`
|
||||
// store path to /run/current-system
|
||||
CurrentSystem *container.Absolute `json:"current_system"`
|
||||
// store path to home-manager activation package
|
||||
ActivationPackage string `json:"activation_package"`
|
||||
}
|
||||
|
||||
func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config {
|
||||
config := &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: pathname,
|
||||
Args: argv,
|
||||
|
||||
Enablements: app.Enablements,
|
||||
|
||||
SystemBus: app.SystemBus,
|
||||
SessionBus: app.SessionBus,
|
||||
DirectWayland: app.DirectWayland,
|
||||
|
||||
Username: "hakurei",
|
||||
Shell: pathShell,
|
||||
Home: pathDataData.Append(app.ID),
|
||||
|
||||
Identity: app.Identity,
|
||||
Groups: app.Groups,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name),
|
||||
Devel: app.Devel,
|
||||
Userns: app.Userns,
|
||||
HostNet: app.HostNet,
|
||||
HostAbstract: app.HostAbstract,
|
||||
Device: app.Device,
|
||||
Tty: app.Tty || flagDropShell,
|
||||
MapRealUID: app.MapRealUID,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
|
||||
},
|
||||
},
|
||||
ExtraPerms: []*hst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
}
|
||||
if app.Multiarch {
|
||||
config.Container.SeccompFlags |= seccomp.AllowMultiarch
|
||||
}
|
||||
if app.Bluetooth {
|
||||
config.Container.SeccompFlags |= seccomp.AllowBluetooth
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func loadAppInfo(name string, beforeFail func()) *appInfo {
|
||||
bundle := new(appInfo)
|
||||
if f, err := os.Open(name); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot open bundle: %v", err)
|
||||
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot parse bundle metadata: %v", err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Printf("cannot close bundle metadata: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if bundle.ID == "" {
|
||||
beforeFail()
|
||||
log.Fatal("application identifier must not be empty")
|
||||
}
|
||||
if bundle.Launcher == nil {
|
||||
beforeFail()
|
||||
log.Fatal("launcher must not be empty")
|
||||
}
|
||||
if bundle.CurrentSystem == nil {
|
||||
beforeFail()
|
||||
log.Fatal("current-system must not be empty")
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
func formatHostname(name string) string {
|
||||
if h, err := os.Hostname(); err != nil {
|
||||
log.Printf("cannot get hostname: %v", err)
|
||||
return "hakurei-" + name
|
||||
} else {
|
||||
return h + "-" + name
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
closureInfo,
|
||||
writeScript,
|
||||
runtimeShell,
|
||||
writeText,
|
||||
@@ -15,18 +17,21 @@
|
||||
runCommand,
|
||||
fetchFromGitHub,
|
||||
|
||||
zstd,
|
||||
nix,
|
||||
sqlite,
|
||||
|
||||
name ? throw "name is required",
|
||||
version ? throw "version is required",
|
||||
pname ? "${name}-${version}",
|
||||
modules ? [ ],
|
||||
nixosModules ? [ ],
|
||||
script ? ''
|
||||
exec "$SHELL" "$@"
|
||||
'',
|
||||
|
||||
id ? name,
|
||||
app_id ? throw "app_id is required",
|
||||
identity ? throw "identity is required",
|
||||
groups ? [ ],
|
||||
userns ? false,
|
||||
net ? true,
|
||||
@@ -52,7 +57,7 @@ let
|
||||
modules = modules ++ [
|
||||
{
|
||||
home = {
|
||||
username = "fortify";
|
||||
username = "hakurei";
|
||||
homeDirectory = "/data/data/${id}";
|
||||
stateVersion = "22.11";
|
||||
};
|
||||
@@ -60,7 +65,7 @@ let
|
||||
];
|
||||
};
|
||||
|
||||
launcher = writeScript "fortify-${pname}" ''
|
||||
launcher = writeScript "hakurei-${pname}" ''
|
||||
#!${runtimeShell} -el
|
||||
${script}
|
||||
'';
|
||||
@@ -72,6 +77,8 @@ let
|
||||
etc.nixpkgs.source = nixpkgs.outPath;
|
||||
systemPackages = [ pkgs.nix ];
|
||||
};
|
||||
|
||||
imports = nixosModules;
|
||||
};
|
||||
nixos = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
@@ -140,7 +147,7 @@ let
|
||||
name
|
||||
version
|
||||
id
|
||||
app_id
|
||||
identity
|
||||
launcher
|
||||
groups
|
||||
userns
|
||||
@@ -164,11 +171,12 @@ let
|
||||
broadcast = { };
|
||||
});
|
||||
|
||||
enablements =
|
||||
(if allow_wayland then 1 else 0)
|
||||
+ (if allow_x11 then 2 else 0)
|
||||
+ (if allow_dbus then 4 else 0)
|
||||
+ (if allow_pulse then 8 else 0);
|
||||
enablements = {
|
||||
wayland = allow_wayland;
|
||||
x11 = allow_x11;
|
||||
dbus = allow_dbus;
|
||||
pulse = allow_pulse;
|
||||
};
|
||||
|
||||
mesa = if gpu then mesaWrappers else null;
|
||||
nix_gl = if gpu then nixGL else null;
|
||||
@@ -177,26 +185,72 @@ let
|
||||
};
|
||||
in
|
||||
|
||||
writeScript "fortify-${pname}-bundle-prelude" ''
|
||||
#!${runtimeShell} -el
|
||||
OUT="$(mktemp -d)"
|
||||
TAR="$(mktemp -u)"
|
||||
set -x
|
||||
stdenv.mkDerivation {
|
||||
name = "${pname}.pkg";
|
||||
inherit version;
|
||||
__structuredAttrs = true;
|
||||
|
||||
nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
|
||||
nix store --store "$OUT" optimise
|
||||
chmod -R +r "$OUT/nix/var"
|
||||
nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19¶llel-compression=true" \
|
||||
"${homeManagerConfiguration.activationPackage}" \
|
||||
"${launcher}" ${if gpu then "${mesaWrappers} ${nixGL}" else ""}
|
||||
mkdir -p "$OUT/etc"
|
||||
tar -C "$OUT/etc" -xf "${etc}/etc.tar"
|
||||
cp "${writeText "bundle.json" info}" "$OUT/bundle.json"
|
||||
nativeBuildInputs = [
|
||||
zstd
|
||||
nix
|
||||
sqlite
|
||||
];
|
||||
|
||||
# creating an intermediate file improves zstd performance
|
||||
tar -C "$OUT" -cf "$TAR" .
|
||||
chmod +w -R "$OUT" && rm -rf "$OUT"
|
||||
buildCommand = ''
|
||||
NIX_ROOT="$(mktemp -d)"
|
||||
export USER="nobody"
|
||||
|
||||
zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
|
||||
rm "$TAR"
|
||||
''
|
||||
# create bootstrap store
|
||||
bootstrapClosureInfo="${
|
||||
closureInfo {
|
||||
rootPaths = [
|
||||
nix
|
||||
nixos.config.system.build.toplevel
|
||||
];
|
||||
}
|
||||
}"
|
||||
echo "copying bootstrap store paths..."
|
||||
mkdir -p "$NIX_ROOT/nix/store"
|
||||
xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
|
||||
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
|
||||
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
|
||||
sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||
chmod -R +r "$NIX_ROOT/nix/var"
|
||||
|
||||
# create binary cache
|
||||
closureInfo="${
|
||||
closureInfo {
|
||||
rootPaths = [
|
||||
homeManagerConfiguration.activationPackage
|
||||
launcher
|
||||
]
|
||||
++ optionals gpu [
|
||||
mesaWrappers
|
||||
nixGL
|
||||
];
|
||||
}
|
||||
}"
|
||||
echo "copying application paths..."
|
||||
TMP_STORE="$(mktemp -d)"
|
||||
mkdir -p "$TMP_STORE/nix/store"
|
||||
xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
|
||||
NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
|
||||
sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||
NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
|
||||
--verbose --log-format raw-with-logs \
|
||||
copy --all --no-check-sigs --to \
|
||||
"file://$NIX_ROOT/res?compression=zstd&compression-level=19¶llel-compression=true"
|
||||
|
||||
# package /etc
|
||||
mkdir -p "$NIX_ROOT/etc"
|
||||
tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
|
||||
|
||||
# write metadata
|
||||
cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
|
||||
|
||||
# create an intermediate file to improve zstd performance
|
||||
INTER="$(mktemp)"
|
||||
tar -C "$NIX_ROOT" -cf "$INTER" .
|
||||
zstd -T0 -19 -fo "$out" "$INTER"
|
||||
'';
|
||||
}
|
||||
334
cmd/hpkg/main.go
Normal file
334
cmd/hpkg/main.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
)
|
||||
|
||||
func init() {
|
||||
hlog.Prepare("hpkg")
|
||||
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
|
||||
log.Fatalf("cannot set $SHELL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagDropShell bool
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
|
||||
|
||||
{
|
||||
var (
|
||||
flagDropShellActivate bool
|
||||
)
|
||||
c.NewCommand("install", "Install an application from its package", func(args []string) error {
|
||||
if len(args) != 1 {
|
||||
log.Println("invalid argument")
|
||||
return syscall.EINVAL
|
||||
}
|
||||
pkgPath := args[0]
|
||||
if !path.IsAbs(pkgPath) {
|
||||
if dir, err := os.Getwd(); err != nil {
|
||||
log.Printf("cannot get current directory: %v", err)
|
||||
return err
|
||||
} else {
|
||||
pkgPath = path.Join(dir, pkgPath)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Look up paths to programs started by hpkg.
|
||||
This is done here to ease error handling as cleanup is not yet required.
|
||||
*/
|
||||
|
||||
var (
|
||||
_ = lookPath("zstd")
|
||||
tar = lookPath("tar")
|
||||
chmod = lookPath("chmod")
|
||||
rm = lookPath("rm")
|
||||
)
|
||||
|
||||
/*
|
||||
Extract package and set up for cleanup.
|
||||
*/
|
||||
|
||||
var workDir *container.Absolute
|
||||
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
|
||||
log.Printf("cannot create temporary directory: %v", err)
|
||||
return err
|
||||
} else if workDir, err = container.NewAbs(p); err != nil {
|
||||
log.Printf("invalid temporary directory: %v", err)
|
||||
return err
|
||||
}
|
||||
cleanup := func() {
|
||||
// should be faster than a native implementation
|
||||
mustRun(chmod, "-R", "+w", workDir.String())
|
||||
mustRun(rm, "-rf", workDir.String())
|
||||
}
|
||||
beforeRunFail.Store(&cleanup)
|
||||
|
||||
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
|
||||
|
||||
/*
|
||||
Parse bundle and app metadata, do pre-install checks.
|
||||
*/
|
||||
|
||||
bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
|
||||
pathSet := pathSetByApp(bundle.ID)
|
||||
|
||||
a := bundle
|
||||
if s, err := os.Stat(pathSet.metaPath.String()); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
cleanup()
|
||||
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
|
||||
return err
|
||||
}
|
||||
// did not modify app, clean installation condition met later
|
||||
} else if s.IsDir() {
|
||||
cleanup()
|
||||
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
||||
return syscall.EBADMSG
|
||||
} else {
|
||||
a = loadAppInfo(pathSet.metaPath.String(), cleanup)
|
||||
if a.ID != bundle.ID {
|
||||
cleanup()
|
||||
log.Printf("app %q claims to have identifier %q",
|
||||
bundle.ID, a.ID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
// sec: should verify credentials
|
||||
}
|
||||
|
||||
if a != bundle {
|
||||
// do not try to re-install
|
||||
if a.NixGL == bundle.NixGL &&
|
||||
a.CurrentSystem == bundle.CurrentSystem &&
|
||||
a.Launcher == bundle.Launcher &&
|
||||
a.ActivationPackage == bundle.ActivationPackage {
|
||||
cleanup()
|
||||
log.Printf("package %q is identical to local application %q",
|
||||
pkgPath, a.ID)
|
||||
return errSuccess
|
||||
}
|
||||
|
||||
// identity determines uid
|
||||
if a.Identity != bundle.Identity {
|
||||
cleanup()
|
||||
log.Printf("package %q identity %d differs from installed %d",
|
||||
pkgPath, bundle.Identity, a.Identity)
|
||||
return syscall.EBADE
|
||||
}
|
||||
|
||||
// sec: should compare version string
|
||||
hlog.Verbosef("installing application %q version %q over local %q",
|
||||
bundle.ID, bundle.Version, a.Version)
|
||||
} else {
|
||||
hlog.Verbosef("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
/*
|
||||
Setup steps for files owned by the target user.
|
||||
*/
|
||||
|
||||
withCacheDir(ctx, "install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + hst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
"mkdir -p etc",
|
||||
"chmod -R +w etc",
|
||||
"rm -rf etc",
|
||||
"cp -dRf $BUNDLE/etc etc",
|
||||
// replace inner /nix
|
||||
"mkdir -p nix",
|
||||
"chmod -R +w nix",
|
||||
"rm -rf nix",
|
||||
"cp -dRf /nix nix",
|
||||
// copy from binary cache
|
||||
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
|
||||
// deduplicate nix store
|
||||
"nix store --offline --store $PWD optimise",
|
||||
// make cache directory world-readable for autoetc
|
||||
"chmod 0755 .",
|
||||
}, workDir, bundle, pathSet, flagDropShell, cleanup)
|
||||
|
||||
if bundle.GPU {
|
||||
withCacheDir(ctx, "mesa-wrappers", []string{
|
||||
// link nixGL mesa wrappers
|
||||
"mkdir -p nix/.nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
|
||||
}, workDir, bundle, pathSet, false, cleanup)
|
||||
}
|
||||
|
||||
/*
|
||||
Activate home-manager generation.
|
||||
*/
|
||||
|
||||
withNixDaemon(ctx, "activate", []string{
|
||||
// clean up broken links
|
||||
"mkdir -p .local/state/{nix,home-manager}",
|
||||
"chmod -R +w .local/state/{nix,home-manager}",
|
||||
"rm -rf .local/state/{nix,home-manager}",
|
||||
// run activation script
|
||||
bundle.ActivationPackage + "/activate",
|
||||
}, false, func(config *hst.Config) *hst.Config { return config },
|
||||
bundle, pathSet, flagDropShellActivate, cleanup)
|
||||
|
||||
/*
|
||||
Installation complete. Write metadata to block re-installs or downgrades.
|
||||
*/
|
||||
|
||||
// serialise metadata to ensure consistency
|
||||
if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot create metadata file: %v", err)
|
||||
return err
|
||||
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot write metadata: %v", err)
|
||||
return err
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Printf("cannot close metadata file: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot rename metadata file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup()
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
|
||||
}
|
||||
|
||||
{
|
||||
var (
|
||||
flagDropShellNixGL bool
|
||||
flagAutoDrivers bool
|
||||
)
|
||||
c.NewCommand("start", "Start an application", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Println("invalid argument")
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
/*
|
||||
Parse app metadata.
|
||||
*/
|
||||
|
||||
id := args[0]
|
||||
pathSet := pathSetByApp(id)
|
||||
a := loadAppInfo(pathSet.metaPath.String(), func() {})
|
||||
if a.ID != id {
|
||||
log.Printf("app %q claims to have identifier %q", id, a.ID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
|
||||
/*
|
||||
Prepare nixGL.
|
||||
*/
|
||||
|
||||
if a.GPU && flagAutoDrivers {
|
||||
withNixDaemon(ctx, "nix-gl", []string{
|
||||
"mkdir -p /nix/.nixGL/auto",
|
||||
"rm -rf /nix/.nixGL/auto",
|
||||
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/opengl " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + a.NixGL,
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + a.NixGL + "#nixVulkanNvidia",
|
||||
}, true, func(config *hst.Config) *hst.Config {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
|
||||
}...)
|
||||
appendGPUFilesystem(config)
|
||||
return config
|
||||
}, a, pathSet, flagDropShellNixGL, func() {})
|
||||
}
|
||||
|
||||
/*
|
||||
Create app configuration.
|
||||
*/
|
||||
|
||||
pathname := a.Launcher
|
||||
argv := make([]string, 1, len(args))
|
||||
if flagDropShell {
|
||||
pathname = pathShell
|
||||
argv[0] = bash
|
||||
} else {
|
||||
argv[0] = a.Launcher.String()
|
||||
}
|
||||
argv = append(argv, args[1:]...)
|
||||
config := a.toHst(pathSet, pathname, argv, flagDropShell)
|
||||
|
||||
/*
|
||||
Expose GPU devices.
|
||||
*/
|
||||
|
||||
if a.GPU {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem,
|
||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
|
||||
appendGPUFilesystem(config)
|
||||
}
|
||||
|
||||
/*
|
||||
Spawn app.
|
||||
*/
|
||||
|
||||
mustRunApp(ctx, config, func() {})
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
|
||||
Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
|
||||
}
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
116
cmd/hpkg/paths.go
Normal file
116
cmd/hpkg/paths.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
const bash = "bash"
|
||||
|
||||
var (
|
||||
dataHome *container.Absolute
|
||||
)
|
||||
|
||||
func init() {
|
||||
// dataHome
|
||||
if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
|
||||
dataHome = a
|
||||
} else {
|
||||
dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
pathBin = container.AbsFHSRoot.Append("bin")
|
||||
|
||||
pathNix = container.MustAbs("/nix/")
|
||||
pathNixStore = pathNix.Append("store/")
|
||||
pathCurrentSystem = container.AbsFHSRun.Append("current-system")
|
||||
pathSwBin = pathCurrentSystem.Append("sw/bin/")
|
||||
pathShell = pathSwBin.Append(bash)
|
||||
|
||||
pathData = container.MustAbs("/data")
|
||||
pathDataData = pathData.Append("data")
|
||||
)
|
||||
|
||||
func lookPath(file string) string {
|
||||
if p, err := exec.LookPath(file); err != nil {
|
||||
log.Fatalf("%s: command not found", file)
|
||||
return ""
|
||||
} else {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
var beforeRunFail = new(atomic.Pointer[func()])
|
||||
|
||||
func mustRun(name string, arg ...string) {
|
||||
hlog.Verbosef("spawning process: %q %q", name, arg)
|
||||
cmd := exec.Command(name, arg...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if f := beforeRunFail.Swap(nil); f != nil {
|
||||
(*f)()
|
||||
}
|
||||
log.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
type appPathSet struct {
|
||||
// ${dataHome}/${id}
|
||||
baseDir *container.Absolute
|
||||
// ${baseDir}/app
|
||||
metaPath *container.Absolute
|
||||
// ${baseDir}/files
|
||||
homeDir *container.Absolute
|
||||
// ${baseDir}/cache
|
||||
cacheDir *container.Absolute
|
||||
// ${baseDir}/cache/nix
|
||||
nixPath *container.Absolute
|
||||
}
|
||||
|
||||
func pathSetByApp(id string) *appPathSet {
|
||||
pathSet := new(appPathSet)
|
||||
pathSet.baseDir = dataHome.Append(id)
|
||||
pathSet.metaPath = pathSet.baseDir.Append("app")
|
||||
pathSet.homeDir = pathSet.baseDir.Append("files")
|
||||
pathSet.cacheDir = pathSet.baseDir.Append("cache")
|
||||
pathSet.nixPath = pathSet.cacheDir.Append("nix")
|
||||
return pathSet
|
||||
}
|
||||
|
||||
func appendGPUFilesystem(config *hst.Config) {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
|
||||
// mali
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali0"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("umplock"), Device: true, Optional: true}},
|
||||
// nvidia
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidiactl"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-modeset"), Device: true, Optional: true}},
|
||||
// nvidia OpenCL/CUDA
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
|
||||
|
||||
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia1"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia3"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia5"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia7"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia9"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia11"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia13"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia15"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia17"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia19"), Device: true, Optional: true}},
|
||||
}...)
|
||||
}
|
||||
60
cmd/hpkg/proc.go
Normal file
60
cmd/hpkg/proc.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var hakureiPath = internal.MustHakureiPath()
|
||||
|
||||
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
st io.WriteCloser
|
||||
)
|
||||
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot pipe: %v", err)
|
||||
} else {
|
||||
if hlog.Load() {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
|
||||
}
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.ExtraFiles = []*os.File{r}
|
||||
st = w
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := json.NewEncoder(st).Encode(config); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot send configuration: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot start hakurei: %v", err)
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
beforeFail()
|
||||
internal.Exit(exitError.ExitCode())
|
||||
} else {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot wait: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
62
cmd/hpkg/test/configuration.nix
Normal file
62
cmd/hpkg/test/configuration.nix
Normal file
@@ -0,0 +1,62 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.alice.home.stateVersion = "24.11";
|
||||
|
||||
# Automatically login on tty1 as a normal user:
|
||||
services.getty.autologinUser = "alice";
|
||||
|
||||
environment = {
|
||||
variables = {
|
||||
SWAYSOCK = "/tmp/sway-ipc.sock";
|
||||
WLR_RENDERER = "pixman";
|
||||
};
|
||||
};
|
||||
|
||||
# Automatically configure and start Sway when logging in on tty1:
|
||||
programs.bash.loginShellInit = ''
|
||||
if [ "$(tty)" = "/dev/tty1" ]; then
|
||||
set -e
|
||||
|
||||
mkdir -p ~/.config/sway
|
||||
(sed s/Mod4/Mod1/ /etc/sway/config &&
|
||||
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
|
||||
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
|
||||
|
||||
sway --validate
|
||||
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
|
||||
fi
|
||||
'';
|
||||
|
||||
programs.sway.enable = true;
|
||||
|
||||
virtualisation = {
|
||||
diskSize = 6 * 1024;
|
||||
|
||||
qemu.options = [
|
||||
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
|
||||
"-vga none -device virtio-gpu-pci"
|
||||
|
||||
# Increase zstd performance:
|
||||
"-smp 8"
|
||||
];
|
||||
};
|
||||
|
||||
environment.hakurei = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/hakurei";
|
||||
users.alice = 0;
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
};
|
||||
}
|
||||
34
cmd/hpkg/test/default.nix
Normal file
34
cmd/hpkg/test/default.nix
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
nixosTest,
|
||||
callPackage,
|
||||
|
||||
system,
|
||||
self,
|
||||
}:
|
||||
let
|
||||
buildPackage = self.buildPackage.${system};
|
||||
in
|
||||
nixosTest {
|
||||
name = "hpkg";
|
||||
nodes.machine = {
|
||||
environment.etc = {
|
||||
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
|
||||
};
|
||||
|
||||
imports = [
|
||||
./configuration.nix
|
||||
|
||||
self.nixosModules.hakurei
|
||||
self.inputs.home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
|
||||
# adapted from nixos sway integration tests
|
||||
|
||||
# testScriptWithTypes:49: error: Cannot call function of unknown type
|
||||
# (machine.succeed if succeed else machine.execute)(
|
||||
# ^
|
||||
# Found 1 error in 1 file (checked 1 source file)
|
||||
skipTypeCheck = true;
|
||||
testScript = builtins.readFile ./test.py;
|
||||
}
|
||||
48
cmd/hpkg/test/foot.nix
Normal file
48
cmd/hpkg/test/foot.nix
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
lib,
|
||||
buildPackage,
|
||||
foot,
|
||||
wayland-utils,
|
||||
inconsolata,
|
||||
}:
|
||||
|
||||
buildPackage {
|
||||
name = "foot";
|
||||
inherit (foot) version;
|
||||
|
||||
identity = 2;
|
||||
id = "org.codeberg.dnkl.foot";
|
||||
|
||||
modules = [
|
||||
{
|
||||
home.packages = [
|
||||
foot
|
||||
|
||||
# For wayland-info:
|
||||
wayland-utils
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
nixosModules = [
|
||||
{
|
||||
# To help with OCR:
|
||||
environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
|
||||
main = {
|
||||
font = "inconsolata:size=14";
|
||||
};
|
||||
colors = rec {
|
||||
foreground = "000000";
|
||||
background = "ffffff";
|
||||
regular2 = foreground;
|
||||
};
|
||||
};
|
||||
|
||||
fonts.packages = [ inconsolata ];
|
||||
}
|
||||
];
|
||||
|
||||
script = ''
|
||||
exec foot "$@"
|
||||
'';
|
||||
}
|
||||
108
cmd/hpkg/test/test.py
Normal file
108
cmd/hpkg/test/test.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import json
|
||||
import shlex
|
||||
|
||||
q = shlex.quote
|
||||
NODE_GROUPS = ["nodes", "floating_nodes"]
|
||||
|
||||
|
||||
def swaymsg(command: str = "", succeed=True, type="command"):
|
||||
assert command != "" or type != "command", "Must specify command or type"
|
||||
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
|
||||
with machine.nested(
|
||||
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
|
||||
):
|
||||
ret = (machine.succeed if succeed else machine.execute)(
|
||||
f"su - alice -c {shell}"
|
||||
)
|
||||
|
||||
# execute also returns a status code, but disregard.
|
||||
if not succeed:
|
||||
_, ret = ret
|
||||
|
||||
if not succeed and not ret:
|
||||
return None
|
||||
|
||||
parsed = json.loads(ret)
|
||||
return parsed
|
||||
|
||||
|
||||
def walk(tree):
|
||||
yield tree
|
||||
for group in NODE_GROUPS:
|
||||
for node in tree.get(group, []):
|
||||
yield from walk(node)
|
||||
|
||||
|
||||
def wait_for_window(pattern):
|
||||
def func(last_chance):
|
||||
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
|
||||
|
||||
if last_chance:
|
||||
nodes = list(nodes)
|
||||
machine.log(f"Last call! Current list of windows: {nodes}")
|
||||
|
||||
return any(pattern in name for name in nodes)
|
||||
|
||||
retry(func)
|
||||
|
||||
|
||||
def collect_state_ui(name):
|
||||
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.ps", "")
|
||||
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.json", "")
|
||||
machine.screenshot(name)
|
||||
|
||||
|
||||
def check_state(name, enablements):
|
||||
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
|
||||
if len(instances) != 1:
|
||||
raise Exception(f"unexpected state length {len(instances)}")
|
||||
instance = next(iter(instances.values()))
|
||||
|
||||
config = instance['config']
|
||||
|
||||
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
|
||||
raise Exception(f"unexpected args {instance['config']['args']}")
|
||||
|
||||
if config['enablements'] != enablements:
|
||||
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
|
||||
|
||||
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check hakurei's version:
|
||||
print(machine.succeed("sudo -u alice -i hakurei version"))
|
||||
|
||||
# Wait for Sway to complete startup:
|
||||
machine.wait_for_file("/run/user/1000/wayland-1")
|
||||
machine.wait_for_file("/tmp/sway-ipc.sock")
|
||||
|
||||
# Prepare hpkg directory:
|
||||
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
|
||||
|
||||
# Install hpkg app:
|
||||
swaymsg("exec hpkg -v install /etc/foot.pkg && touch /tmp/hpkg-install-ok")
|
||||
machine.wait_for_file("/tmp/hpkg-install-ok")
|
||||
|
||||
# Start app (foot) with Wayland enablement:
|
||||
swaymsg("exec hpkg -v start org.codeberg.dnkl.foot")
|
||||
wait_for_window("hakurei@machine-foot")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
|
||||
collect_state_ui("app_wayland")
|
||||
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
|
||||
# Verify acl on XDG_RUNTIME_DIR:
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
swaymsg("exit", succeed=False)
|
||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||
|
||||
# Print hakurei runDir contents:
|
||||
print(machine.succeed("find /run/user/1000/hakurei"))
|
||||
108
cmd/hpkg/with.go
Normal file
108
cmd/hpkg/with.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
)
|
||||
|
||||
func withNixDaemon(
|
||||
ctx context.Context,
|
||||
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||
) {
|
||||
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: pathShell,
|
||||
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
|
||||
// start nix-daemon
|
||||
"nix-daemon --store / & " +
|
||||
// wait for socket to appear
|
||||
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
|
||||
// create directory so nix stops complaining
|
||||
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
|
||||
strings.Join(command, " && ") +
|
||||
// terminate nix-daemon
|
||||
" && pkill nix-daemon",
|
||||
},
|
||||
|
||||
Username: "hakurei",
|
||||
Shell: pathShell,
|
||||
Home: pathDataData.Append(app.ID),
|
||||
ExtraPerms: []*hst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
|
||||
Identity: app.Identity,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
Userns: true, // nix sandbox requires userns
|
||||
HostNet: net,
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
|
||||
},
|
||||
},
|
||||
}), dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func withCacheDir(
|
||||
ctx context.Context,
|
||||
action string, command []string, workDir *container.Absolute,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
mustRunAppDropShell(ctx, &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: pathShell,
|
||||
Args: []string{bash, "-lc", strings.Join(command, " && ")},
|
||||
|
||||
Username: "nixos",
|
||||
Shell: pathShell,
|
||||
Home: pathDataData.Append(app.ID, "cache"),
|
||||
ExtraPerms: []*hst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
{Path: workDir, Execute: true},
|
||||
},
|
||||
|
||||
Identity: app.Identity,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
|
||||
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
|
||||
},
|
||||
},
|
||||
}, dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
|
||||
if dropShell {
|
||||
config.Args = []string{bash, "-l"}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
beforeFail()
|
||||
internal.Exit(0)
|
||||
}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
}
|
||||
@@ -13,22 +13,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
fsuConfFile = "/etc/fsurc"
|
||||
envShim = "FORTIFY_SHIM"
|
||||
envAID = "FORTIFY_APP_ID"
|
||||
envGroups = "FORTIFY_GROUPS"
|
||||
hsuConfFile = "/etc/hsurc"
|
||||
envShim = "HAKUREI_SHIM"
|
||||
envAID = "HAKUREI_APP_ID"
|
||||
envGroups = "HAKUREI_GROUPS"
|
||||
|
||||
PR_SET_NO_NEW_PRIVS = 0x26
|
||||
)
|
||||
|
||||
var (
|
||||
Fmain = compPoison
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("fsu: ")
|
||||
log.SetPrefix("hsu: ")
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
@@ -40,20 +35,16 @@ func main() {
|
||||
log.Fatal("this program must not be started by root")
|
||||
}
|
||||
|
||||
var fmain string
|
||||
if p, ok := checkPath(Fmain); !ok {
|
||||
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
|
||||
} else {
|
||||
fmain = p
|
||||
}
|
||||
|
||||
var toolPath string
|
||||
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
||||
if p, err := os.Readlink(pexe); err != nil {
|
||||
log.Fatalf("cannot read parent executable path: %v", err)
|
||||
} else if strings.HasSuffix(p, " (deleted)") {
|
||||
log.Fatal("fortify executable has been deleted")
|
||||
} else if p != fmain {
|
||||
log.Fatal("this program must be started by fortify")
|
||||
log.Fatal("hakurei executable has been deleted")
|
||||
} else if p != mustCheckPath(hmain) {
|
||||
log.Fatal("this program must be started by hakurei")
|
||||
} else {
|
||||
toolPath = p
|
||||
}
|
||||
|
||||
// uid = 1000000 +
|
||||
@@ -61,27 +52,27 @@ func main() {
|
||||
// aid
|
||||
uid := 1000000
|
||||
|
||||
// refuse to run if fsurc is not protected correctly
|
||||
if s, err := os.Stat(fsuConfFile); err != nil {
|
||||
// refuse to run if hsurc is not protected correctly
|
||||
if s, err := os.Stat(hsuConfFile); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if s.Mode().Perm() != 0400 {
|
||||
log.Fatal("bad fsurc perm")
|
||||
log.Fatal("bad hsurc perm")
|
||||
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
|
||||
log.Fatal("fsurc must be owned by uid 0")
|
||||
log.Fatal("hsurc must be owned by uid 0")
|
||||
}
|
||||
|
||||
// authenticate before accepting user input
|
||||
if f, err := os.Open(fsuConfFile); err != nil {
|
||||
if f, err := os.Open(hsuConfFile); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if fid, ok := mustParseConfig(f, puid); !ok {
|
||||
log.Fatalf("uid %d is not in the fsurc file", puid)
|
||||
log.Fatalf("uid %d is not in the hsurc file", puid)
|
||||
} else {
|
||||
uid += fid * 10000
|
||||
}
|
||||
|
||||
// allowed aid range 0 to 9999
|
||||
if as, ok := os.LookupEnv(envAID); !ok {
|
||||
log.Fatal("FORTIFY_APP_ID not set")
|
||||
log.Fatal("HAKUREI_APP_ID not set")
|
||||
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
|
||||
log.Fatal("invalid aid")
|
||||
} else {
|
||||
@@ -91,12 +82,12 @@ func main() {
|
||||
// pass through setup fd to shim
|
||||
var shimSetupFd string
|
||||
if s, ok := os.LookupEnv(envShim); !ok {
|
||||
// fortify requests target uid
|
||||
// hakurei requests target uid
|
||||
// print resolved uid and exit
|
||||
fmt.Print(uid)
|
||||
os.Exit(0)
|
||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
||||
log.Fatal("FORTIFY_SHIM holds an invalid value")
|
||||
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
||||
} else {
|
||||
shimSetupFd = s
|
||||
}
|
||||
@@ -133,7 +124,7 @@ func main() {
|
||||
panic("uid out of bounds")
|
||||
}
|
||||
|
||||
// careful! users in the allowlist is effectively allowed to drop groups via fsu
|
||||
// careful! users in the allowlist is effectively allowed to drop groups via hsu
|
||||
|
||||
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
||||
log.Fatalf("cannot set gid: %v", err)
|
||||
@@ -147,13 +138,9 @@ func main() {
|
||||
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
||||
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||
}
|
||||
if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
log.Fatalf("cannot start shim: %v", err)
|
||||
}
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func checkPath(p string) (string, bool) {
|
||||
return p, p != compPoison && p != "" && path.IsAbs(p)
|
||||
}
|
||||
23
cmd/hsu/package.nix
Normal file
23
cmd/hsu/package.nix
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
lib,
|
||||
buildGoModule,
|
||||
hakurei ? abort "hakurei package required",
|
||||
}:
|
||||
|
||||
buildGoModule {
|
||||
pname = "${hakurei.pname}-hsu";
|
||||
inherit (hakurei) version;
|
||||
|
||||
src = ./.;
|
||||
inherit (hakurei) vendorHash;
|
||||
env.CGO_ENABLED = 0;
|
||||
|
||||
preBuild = ''
|
||||
go mod init hsu >& /dev/null
|
||||
'';
|
||||
|
||||
ldflags = lib.attrsets.foldlAttrs (
|
||||
ldflags: name: value:
|
||||
ldflags ++ [ "-X main.${name}=${value}" ]
|
||||
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; };
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
|
||||
if ok {
|
||||
// allowed fid range 0 to 99
|
||||
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
|
||||
return -1, false, fmt.Errorf("invalid fortify uid on line %d", line)
|
||||
return -1, false, fmt.Errorf("invalid identity on line %d", line)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func Test_parseConfig(t *testing.T) {
|
||||
{"empty", 0, -1, "", ``},
|
||||
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
||||
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
|
||||
{"invalid fid", 1000, -1, "invalid fortify uid on line 1", `1000 f`},
|
||||
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
|
||||
{"match", 1000, 0, "", `1000 0`},
|
||||
}
|
||||
|
||||
20
cmd/hsu/path.go
Normal file
20
cmd/hsu/path.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path"
|
||||
)
|
||||
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
|
||||
var (
|
||||
hmain = compPoison
|
||||
)
|
||||
|
||||
func mustCheckPath(p string) string {
|
||||
if p != compPoison && p != "" && path.IsAbs(p) {
|
||||
return p
|
||||
}
|
||||
log.Fatal("this program is compiled incorrectly")
|
||||
return compPoison
|
||||
}
|
||||
65
command/builder.go
Normal file
65
command/builder.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New initialises a root Node.
|
||||
func New(output io.Writer, logf LogFunc, name string, early HandlerFunc) Command {
|
||||
c := rootNode{newNode(output, logf, name, "")}
|
||||
c.f = early
|
||||
return c
|
||||
}
|
||||
|
||||
func newNode(output io.Writer, logf LogFunc, name, usage string) *node {
|
||||
n := &node{
|
||||
name: name, usage: usage,
|
||||
out: output, logf: logf,
|
||||
set: flag.NewFlagSet(name, flag.ContinueOnError),
|
||||
}
|
||||
n.set.SetOutput(output)
|
||||
n.set.Usage = func() {
|
||||
_ = n.writeHelp()
|
||||
if n.suffix.Len() > 0 {
|
||||
_, _ = fmt.Fprintln(output, "Flags:")
|
||||
n.set.PrintDefaults()
|
||||
_, _ = fmt.Fprintln(output)
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *node) Command(name, usage string, f HandlerFunc) Node {
|
||||
n.NewCommand(name, usage, f)
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *node) NewCommand(name, usage string, f HandlerFunc) Flag[Node] {
|
||||
if f == nil {
|
||||
panic("invalid handler")
|
||||
}
|
||||
if name == "" || usage == "" {
|
||||
panic("invalid subcommand")
|
||||
}
|
||||
|
||||
s := newNode(n.out, n.logf, name, usage)
|
||||
s.f = f
|
||||
if !n.adopt(s) {
|
||||
panic("attempted to initialise subcommand with non-unique name")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (n *node) New(name, usage string) Node {
|
||||
if name == "" || usage == "" {
|
||||
panic("invalid subcommand tree")
|
||||
}
|
||||
s := newNode(n.out, n.logf, name, usage)
|
||||
if !n.adopt(s) {
|
||||
panic("attempted to initialise subcommand tree with non-unique name")
|
||||
}
|
||||
return s
|
||||
}
|
||||
56
command/builder_test.go
Normal file
56
command/builder_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
c := command.New(nil, nil, "test", nil)
|
||||
stubHandler := func([]string) error { panic("unreachable") }
|
||||
|
||||
t.Run("nil direct handler", func(t *testing.T) {
|
||||
defer checkRecover(t, "Command", "invalid handler")
|
||||
c.Command("name", "usage", nil)
|
||||
})
|
||||
|
||||
t.Run("direct zero length", func(t *testing.T) {
|
||||
wantPanic := "invalid subcommand"
|
||||
t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("", "usage", stubHandler) })
|
||||
t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("name", "", stubHandler) })
|
||||
})
|
||||
|
||||
t.Run("direct adopt unique names", func(t *testing.T) {
|
||||
c.Command("d0", "usage", stubHandler)
|
||||
c.Command("d1", "usage", stubHandler)
|
||||
})
|
||||
|
||||
t.Run("direct adopt non-unique name", func(t *testing.T) {
|
||||
defer checkRecover(t, "Command", "attempted to initialise subcommand with non-unique name")
|
||||
c.Command("d0", "usage", stubHandler)
|
||||
})
|
||||
|
||||
t.Run("zero length", func(t *testing.T) {
|
||||
wantPanic := "invalid subcommand tree"
|
||||
t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("", "usage") })
|
||||
t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("name", "") })
|
||||
})
|
||||
|
||||
t.Run("direct adopt unique names", func(t *testing.T) {
|
||||
c.New("t0", "usage")
|
||||
c.New("t1", "usage")
|
||||
})
|
||||
|
||||
t.Run("direct adopt non-unique name", func(t *testing.T) {
|
||||
defer checkRecover(t, "Command", "attempted to initialise subcommand tree with non-unique name")
|
||||
c.New("t0", "usage")
|
||||
})
|
||||
}
|
||||
|
||||
func checkRecover(t *testing.T, name, wantPanic string) {
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("%s: panic = %v; wantPanic %v",
|
||||
name, r, wantPanic)
|
||||
}
|
||||
}
|
||||
55
command/command.go
Normal file
55
command/command.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Package command implements generic nested command parsing.
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UsageInternal causes the command to be hidden from help text when set as the usage string.
|
||||
const UsageInternal = "internal"
|
||||
|
||||
type (
|
||||
// HandlerFunc is called when matching a directly handled subcommand tree.
|
||||
HandlerFunc = func(args []string) error
|
||||
|
||||
// LogFunc is the function signature of a printf function.
|
||||
LogFunc = func(format string, a ...any)
|
||||
|
||||
// FlagDefiner is a deferred flag definer value, usually encapsulating the default value.
|
||||
FlagDefiner interface {
|
||||
// Define defines the flag in set.
|
||||
Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string)
|
||||
}
|
||||
|
||||
Flag[T any] interface {
|
||||
// Flag defines a generic flag type in Node's flag set.
|
||||
Flag(p any, name string, value FlagDefiner, usage string) T
|
||||
}
|
||||
|
||||
Command interface {
|
||||
Parse(arguments []string) error
|
||||
|
||||
// MustParse determines exit outcomes for Parse errors
|
||||
// and calls handleError if [HandlerFunc] returns a non-nil error.
|
||||
MustParse(arguments []string, handleError func(error))
|
||||
|
||||
baseNode[Command]
|
||||
}
|
||||
Node baseNode[Node]
|
||||
|
||||
baseNode[T any] interface {
|
||||
// Command appends a subcommand with direct command handling.
|
||||
Command(name, usage string, f HandlerFunc) T
|
||||
|
||||
// New returns a new subcommand tree.
|
||||
New(name, usage string) (sub Node)
|
||||
// NewCommand returns a new subcommand with direct command handling.
|
||||
NewCommand(name, usage string, f HandlerFunc) (sub Flag[Node])
|
||||
|
||||
// PrintHelp prints a help message to the configured writer.
|
||||
PrintHelp()
|
||||
|
||||
Flag[T]
|
||||
}
|
||||
)
|
||||
77
command/flag.go
Normal file
77
command/flag.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FlagError wraps errors returned by [flag].
|
||||
type FlagError struct{ error }
|
||||
|
||||
func (e FlagError) Success() bool { return errors.Is(e.error, flag.ErrHelp) }
|
||||
func (e FlagError) Is(target error) bool {
|
||||
return (e.error == nil && target == nil) ||
|
||||
((e.error != nil && target != nil) && e.error.Error() == target.Error())
|
||||
}
|
||||
|
||||
func (n *node) Flag(p any, name string, value FlagDefiner, usage string) Node {
|
||||
value.Define(&n.suffix, n.set, p, name, usage)
|
||||
return n
|
||||
}
|
||||
|
||||
// StringFlag is the default value of a string flag.
|
||||
type StringFlag string
|
||||
|
||||
func (v StringFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
|
||||
set.StringVar(p.(*string), name, string(v), usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + " <value>]")
|
||||
}
|
||||
|
||||
// IntFlag is the default value of an int flag.
|
||||
type IntFlag int
|
||||
|
||||
func (v IntFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
|
||||
set.IntVar(p.(*int), name, int(v), usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + " <int>]")
|
||||
}
|
||||
|
||||
// BoolFlag is the default value of a bool flag.
|
||||
type BoolFlag bool
|
||||
|
||||
func (v BoolFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
|
||||
set.BoolVar(p.(*bool), name, bool(v), usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + "]")
|
||||
}
|
||||
|
||||
// RepeatableFlag implements an ordered, repeatable string flag.
|
||||
type RepeatableFlag []string
|
||||
|
||||
func (r *RepeatableFlag) String() string {
|
||||
if r == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return strings.Join(*r, " ")
|
||||
}
|
||||
|
||||
func (r *RepeatableFlag) Set(v string) error {
|
||||
*r = append(*r, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepeatableFlag) Define(b *strings.Builder, set *flag.FlagSet, _ any, name, usage string) {
|
||||
set.Var(r, name, usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + " <value>]")
|
||||
}
|
||||
|
||||
// this has no effect on parse outcome
|
||||
func prettyFlag(name string) string {
|
||||
switch len(name) {
|
||||
case 0:
|
||||
panic("zero length flag name")
|
||||
case 1:
|
||||
return "-" + name
|
||||
default:
|
||||
return "--" + name
|
||||
}
|
||||
}
|
||||
53
command/help.go
Normal file
53
command/help.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
var ErrHelp = errors.New("help requested")
|
||||
|
||||
func (n *node) PrintHelp() { _ = n.writeHelp() }
|
||||
|
||||
func (n *node) writeHelp() error {
|
||||
if _, err := fmt.Fprintf(n.out,
|
||||
"\nUsage:\t%s [-h | --help]%s COMMAND [OPTIONS]\n",
|
||||
strings.Join(append(n.prefix, n.name), " "), &n.suffix,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if n.child != nil {
|
||||
if _, err := fmt.Fprint(n.out, "\nCommands:\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(n.out, 0, 1, 4, ' ', 0)
|
||||
if err := n.child.writeCommands(tw); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := n.out.Write([]byte{'\n'})
|
||||
if err == nil {
|
||||
err = ErrHelp
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *node) writeCommands(w io.Writer) error {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if n.usage != UsageInternal {
|
||||
if _, err := fmt.Fprintf(w, "\t%s\t%s\n", n.name, n.usage); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return n.next.writeCommands(w)
|
||||
}
|
||||
40
command/node.go
Normal file
40
command/node.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
child, next *node
|
||||
name, usage string
|
||||
|
||||
out io.Writer
|
||||
logf LogFunc
|
||||
|
||||
prefix []string
|
||||
suffix strings.Builder
|
||||
|
||||
f HandlerFunc
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
func (n *node) adopt(v *node) bool {
|
||||
if n.child != nil {
|
||||
return n.child.append(v)
|
||||
}
|
||||
n.child = v
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *node) append(v *node) bool {
|
||||
if n.name == v.name {
|
||||
return false
|
||||
}
|
||||
if n.next != nil {
|
||||
return n.next.append(v)
|
||||
}
|
||||
n.next = v
|
||||
return true
|
||||
}
|
||||
105
command/parse.go
Normal file
105
command/parse.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyTree = errors.New("subcommand tree has no nodes")
|
||||
ErrNoMatch = errors.New("did not match any subcommand")
|
||||
)
|
||||
|
||||
func (n *node) Parse(arguments []string) error {
|
||||
if n.usage == "" { // root node has zero length usage
|
||||
if n.next != nil {
|
||||
panic("invalid toplevel state")
|
||||
}
|
||||
goto match
|
||||
}
|
||||
|
||||
if len(arguments) == 0 {
|
||||
// unreachable: zero length args cause upper level to return with a help message
|
||||
panic("attempted to parse with zero length args")
|
||||
}
|
||||
if arguments[0] != n.name {
|
||||
if n.next == nil {
|
||||
n.printf("%q is not a valid command", arguments[0])
|
||||
return ErrNoMatch
|
||||
}
|
||||
n.next.prefix = n.prefix
|
||||
return n.next.Parse(arguments)
|
||||
}
|
||||
arguments = arguments[1:]
|
||||
|
||||
match:
|
||||
if n.child != nil {
|
||||
// propagate help prefix early: flag set usage dereferences help
|
||||
n.child.prefix = append(n.prefix, n.name)
|
||||
}
|
||||
|
||||
if n.set.Parsed() {
|
||||
panic("invalid set state")
|
||||
}
|
||||
if err := n.set.Parse(arguments); err != nil {
|
||||
return FlagError{err}
|
||||
}
|
||||
args := n.set.Args()
|
||||
|
||||
if n.child != nil {
|
||||
if n.f != nil {
|
||||
if n.usage != "" { // root node early special case
|
||||
panic("invalid subcommand tree state")
|
||||
}
|
||||
|
||||
// special case: root node calls HandlerFunc for initialisation
|
||||
if err := n.f(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return n.writeHelp()
|
||||
}
|
||||
return n.child.Parse(args)
|
||||
}
|
||||
|
||||
if n.f == nil {
|
||||
n.printf("%q has no subcommands", n.name)
|
||||
return ErrEmptyTree
|
||||
}
|
||||
return n.f(args)
|
||||
}
|
||||
|
||||
func (n *node) printf(format string, a ...any) {
|
||||
if n.logf == nil {
|
||||
log.Printf(format, a...)
|
||||
} else {
|
||||
n.logf(format, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) MustParse(arguments []string, handleError func(error)) {
|
||||
switch err := n.Parse(arguments); err {
|
||||
case nil:
|
||||
return
|
||||
case ErrHelp:
|
||||
os.Exit(0)
|
||||
case ErrNoMatch:
|
||||
os.Exit(1)
|
||||
case ErrEmptyTree:
|
||||
os.Exit(1)
|
||||
default:
|
||||
var flagError FlagError
|
||||
if !errors.As(err, &flagError) { // returned by HandlerFunc
|
||||
handleError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if flagError.Success() {
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
344
command/parse_test.go
Normal file
344
command/parse_test.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
buildTree func(wout, wlog io.Writer) command.Command
|
||||
args []string
|
||||
want string
|
||||
wantLog string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"d=0 empty sub",
|
||||
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
|
||||
[]string{""},
|
||||
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=0 empty sub garbage",
|
||||
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=0 no match",
|
||||
buildTestCommand,
|
||||
[]string{"nonexistent"},
|
||||
"", "test: \"nonexistent\" is not a valid command\n", command.ErrNoMatch,
|
||||
},
|
||||
{
|
||||
"d=0 direct error",
|
||||
buildTestCommand,
|
||||
[]string{"error"},
|
||||
"", "", errSuccess,
|
||||
},
|
||||
{
|
||||
"d=0 direct error garbage",
|
||||
buildTestCommand,
|
||||
[]string{"error", "0", "1", "2"},
|
||||
"", "", errSuccess,
|
||||
},
|
||||
{
|
||||
"d=0 direct success out of order",
|
||||
buildTestCommand,
|
||||
[]string{"succeed"},
|
||||
"", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 direct success output",
|
||||
buildTestCommand,
|
||||
[]string{"print", "0", "1", "2"},
|
||||
"012", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 out of order string flag",
|
||||
buildTestCommand,
|
||||
[]string{"string", "--string", "64d3b4b7b21788585845060e2199a78f"},
|
||||
"flag provided but not defined: -string\n\nUsage:\ttest string [-h | --help] COMMAND [OPTIONS]\n\n", "",
|
||||
errors.New("flag provided but not defined: -string"),
|
||||
},
|
||||
{
|
||||
"d=0 string flag",
|
||||
buildTestCommand,
|
||||
[]string{"--string", "64d3b4b7b21788585845060e2199a78f", "string"},
|
||||
"64d3b4b7b21788585845060e2199a78f", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 int flag",
|
||||
buildTestCommand,
|
||||
[]string{"--int", "2147483647", "int"},
|
||||
"2147483647", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 repeat flag",
|
||||
buildTestCommand,
|
||||
[]string{"--repeat", "0", "--repeat", "1", "--repeat", "2", "--repeat", "3", "--repeat", "4", "repeat"},
|
||||
"[0 1 2 3 4]", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 bool flag",
|
||||
buildTestCommand,
|
||||
[]string{"-v", "succeed"},
|
||||
"", "test: verbose\n", nil,
|
||||
},
|
||||
{
|
||||
"d=0 bool flag early error",
|
||||
buildTestCommand,
|
||||
[]string{"--fail", "succeed"},
|
||||
"", "", errSuccess,
|
||||
},
|
||||
|
||||
{
|
||||
"d=1 empty sub",
|
||||
buildTestCommand,
|
||||
[]string{"empty"},
|
||||
"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=1 empty sub garbage",
|
||||
buildTestCommand,
|
||||
[]string{"empty", "a", "b", "c", "d"},
|
||||
"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=1 empty sub help",
|
||||
buildTestCommand,
|
||||
[]string{"empty", "-h"},
|
||||
"\nUsage:\ttest empty [-h | --help] COMMAND [OPTIONS]\n\n", "", flag.ErrHelp,
|
||||
},
|
||||
{
|
||||
"d=1 no match",
|
||||
buildTestCommand,
|
||||
[]string{"join", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
|
||||
"", "test: \"23aa3bb0\" is not a valid command\n", command.ErrNoMatch,
|
||||
},
|
||||
{
|
||||
"d=1 direct success out",
|
||||
buildTestCommand,
|
||||
[]string{"join", "out", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
|
||||
"23aa3bb0, 34986782, d8859355, cd9ac317", "", nil,
|
||||
},
|
||||
{
|
||||
"d=1 direct success log",
|
||||
buildTestCommand,
|
||||
[]string{"join", "log", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
|
||||
"", "test: 23aa3bb0, 34986782, d8859355, cd9ac317\n", nil,
|
||||
},
|
||||
|
||||
{
|
||||
"d=4 empty sub",
|
||||
buildTestCommand,
|
||||
[]string{"deep", "d=2", "d=3", "d=4"},
|
||||
"", "test: \"d=4\" has no subcommands\n", command.ErrEmptyTree},
|
||||
|
||||
{
|
||||
"d=0 help",
|
||||
buildTestCommand,
|
||||
[]string{},
|
||||
`
|
||||
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
error return an error
|
||||
print wraps Fprint
|
||||
string print string passed by flag
|
||||
int print int passed by flag
|
||||
repeat print repeated values passed by flag
|
||||
empty empty subcommand
|
||||
join wraps strings.Join
|
||||
succeed this command succeeds
|
||||
deep top level of command tree with various levels
|
||||
|
||||
`, "", command.ErrHelp,
|
||||
},
|
||||
{
|
||||
"d=0 help flag",
|
||||
buildTestCommand,
|
||||
[]string{"-h"},
|
||||
`
|
||||
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
error return an error
|
||||
print wraps Fprint
|
||||
string print string passed by flag
|
||||
int print int passed by flag
|
||||
repeat print repeated values passed by flag
|
||||
empty empty subcommand
|
||||
join wraps strings.Join
|
||||
succeed this command succeeds
|
||||
deep top level of command tree with various levels
|
||||
|
||||
Flags:
|
||||
-fail
|
||||
fail early
|
||||
-int int
|
||||
store value for the "int" command (default -1)
|
||||
-repeat value
|
||||
store value for the "repeat" command
|
||||
-string string
|
||||
store value for the "string" command (default "default")
|
||||
-v verbose output
|
||||
|
||||
`, "", flag.ErrHelp,
|
||||
},
|
||||
|
||||
{
|
||||
"d=1 help",
|
||||
buildTestCommand,
|
||||
[]string{"join"},
|
||||
`
|
||||
Usage: test join [-h | --help] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
out write result to wout
|
||||
log log result to wlog
|
||||
|
||||
`, "", command.ErrHelp,
|
||||
},
|
||||
{
|
||||
"d=1 help flag",
|
||||
buildTestCommand,
|
||||
[]string{"join", "-h"},
|
||||
`
|
||||
Usage: test join [-h | --help] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
out write result to wout
|
||||
log log result to wlog
|
||||
|
||||
`, "", flag.ErrHelp,
|
||||
},
|
||||
|
||||
{
|
||||
"d=2 help",
|
||||
buildTestCommand,
|
||||
[]string{"deep", "d=2"},
|
||||
`
|
||||
Usage: test deep d=2 [-h | --help] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
d=3 relative third level
|
||||
|
||||
`, "", command.ErrHelp,
|
||||
},
|
||||
{
|
||||
"d=2 help flag",
|
||||
buildTestCommand,
|
||||
[]string{"deep", "d=2", "-h"},
|
||||
`
|
||||
Usage: test deep d=2 [-h | --help] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
d=3 relative third level
|
||||
|
||||
`, "", flag.ErrHelp,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
|
||||
c := tc.buildTree(wout, wlog)
|
||||
|
||||
if err := c.Parse(tc.args); !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Parse: error = %v; wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
if got := wout.String(); got != tc.want {
|
||||
t.Errorf("Parse: %s want %s", got, tc.want)
|
||||
}
|
||||
if gotLog := wlog.String(); gotLog != tc.wantLog {
|
||||
t.Errorf("Parse: log = %s wantLog %s", gotLog, tc.wantLog)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
errJoinLen = errors.New("not enough arguments to join")
|
||||
errSuccess = errors.New("success")
|
||||
)
|
||||
|
||||
func buildTestCommand(wout, wlog io.Writer) (c command.Command) {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagFail bool
|
||||
|
||||
flagString string
|
||||
flagInt int
|
||||
flagRepeat command.RepeatableFlag
|
||||
)
|
||||
|
||||
logf := newLogFunc(wlog)
|
||||
c = command.New(wout, logf, "test", func([]string) error {
|
||||
if flagVerbose {
|
||||
logf("verbose")
|
||||
}
|
||||
if flagFail {
|
||||
return errSuccess
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "verbose output").
|
||||
Flag(&flagFail, "fail", command.BoolFlag(false), "fail early").
|
||||
Command("error", "return an error", func([]string) error {
|
||||
return errSuccess
|
||||
}).
|
||||
Command("print", "wraps Fprint", func(args []string) error {
|
||||
a := make([]any, len(args))
|
||||
for i, v := range args {
|
||||
a[i] = v
|
||||
}
|
||||
_, err := fmt.Fprint(wout, a...)
|
||||
return err
|
||||
}).
|
||||
Flag(&flagString, "string", command.StringFlag("default"), "store value for the \"string\" command").
|
||||
Command("string", "print string passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagString); return err }).
|
||||
Flag(&flagInt, "int", command.IntFlag(-1), "store value for the \"int\" command").
|
||||
Command("int", "print int passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagInt); return err }).
|
||||
Flag(nil, "repeat", &flagRepeat, "store value for the \"repeat\" command").
|
||||
Command("repeat", "print repeated values passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagRepeat); return err })
|
||||
|
||||
c.New("empty", "empty subcommand")
|
||||
c.New("hidden", command.UsageInternal)
|
||||
|
||||
c.New("join", "wraps strings.Join").
|
||||
Command("out", "write result to wout", func(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errJoinLen
|
||||
}
|
||||
_, err := fmt.Fprint(wout, strings.Join(args[:len(args)-1], args[len(args)-1]))
|
||||
return err
|
||||
}).
|
||||
Command("log", "log result to wlog", func(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errJoinLen
|
||||
}
|
||||
logf("%s", strings.Join(args[:len(args)-1], args[len(args)-1]))
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Command("succeed", "this command succeeds", func([]string) error { return nil })
|
||||
|
||||
c.New("deep", "top level of command tree with various levels").
|
||||
New("d=2", "relative second level").
|
||||
New("d=3", "relative third level").
|
||||
New("d=4", "relative fourth level")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func newLogFunc(w io.Writer) command.LogFunc { return log.New(w, "test: ", 0).Printf }
|
||||
54
command/unreachable_test.go
Normal file
54
command/unreachable_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseUnreachable(t *testing.T) {
|
||||
// top level bypasses name matching and recursive calls to Parse
|
||||
// returns when encountering zero-length args
|
||||
t.Run("zero-length args", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
|
||||
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
|
||||
})
|
||||
|
||||
// top level must not have siblings
|
||||
t.Run("toplevel siblings", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "invalid toplevel state")
|
||||
n := newNode(panicWriter{}, nil, " ", "")
|
||||
n.append(newNode(panicWriter{}, nil, " ", " "))
|
||||
_ = n.Parse(nil)
|
||||
})
|
||||
|
||||
// a node with descendents must not have a direct handler
|
||||
t.Run("sub handle conflict", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "invalid subcommand tree state")
|
||||
n := newNode(panicWriter{}, nil, " ", " ")
|
||||
n.adopt(newNode(panicWriter{}, nil, " ", " "))
|
||||
n.f = func([]string) error { panic("unreachable") }
|
||||
_ = n.Parse([]string{" "})
|
||||
})
|
||||
|
||||
// this would only happen if a node was matched twice
|
||||
t.Run("parsed flag set", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "invalid set state")
|
||||
n := newNode(panicWriter{}, nil, " ", "")
|
||||
set := flag.NewFlagSet("parsed", flag.ContinueOnError)
|
||||
set.SetOutput(panicWriter{})
|
||||
_ = set.Parse(nil)
|
||||
n.set = set
|
||||
_ = n.Parse(nil)
|
||||
})
|
||||
}
|
||||
|
||||
type panicWriter struct{}
|
||||
|
||||
func (p panicWriter) Write([]byte) (int, error) { panic("unreachable") }
|
||||
|
||||
func checkRecover(t *testing.T, name, wantPanic string) {
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("%s: panic = %v; wantPanic %v",
|
||||
name, r, wantPanic)
|
||||
}
|
||||
}
|
||||
14
command/wrap.go
Normal file
14
command/wrap.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package command
|
||||
|
||||
// the top level node wants [Command] returned for its builder methods
|
||||
type rootNode struct{ *node }
|
||||
|
||||
func (r rootNode) Command(name, usage string, f HandlerFunc) Command {
|
||||
r.node.Command(name, usage, f)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r rootNode) Flag(p any, name string, value FlagDefiner, usage string) Command {
|
||||
r.node.Flag(p, name, value, usage)
|
||||
return r
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
#compdef fortify
|
||||
|
||||
_fortify_app() {
|
||||
__fortify_files
|
||||
return $?
|
||||
}
|
||||
|
||||
_fortify_run() {
|
||||
_arguments \
|
||||
'--id[App ID, leave empty to disable security context app_id]:id' \
|
||||
'-a[Fortify application ID]: :_numbers' \
|
||||
'-g[Groups inherited by the app process]: :_groups' \
|
||||
'-d[Application home directory]: :_files -/' \
|
||||
'-u[Passwd name within sandbox]: :_users' \
|
||||
'--wayland[Share Wayland socket]' \
|
||||
'-X[Share X11 socket and allow connection]' \
|
||||
'--dbus[Proxy D-Bus connection]' \
|
||||
'--pulse[Share PulseAudio socket and cookie]' \
|
||||
'--dbus-config[Path to D-Bus proxy config file]: :_files -g "*.json"' \
|
||||
'--dbus-system[Path to system D-Bus proxy config file]: :_files -g "*.json"' \
|
||||
'--mpris[Allow owning MPRIS D-Bus path]' \
|
||||
'--dbus-log[Force logging in the D-Bus proxy]'
|
||||
}
|
||||
|
||||
_fortify_ps() {
|
||||
_arguments \
|
||||
'--short[Print instance id]'
|
||||
}
|
||||
|
||||
_fortify_show() {
|
||||
_alternative \
|
||||
'instances:domains:__fortify_instances' \
|
||||
'files:files:__fortify_files'
|
||||
}
|
||||
|
||||
__fortify_files() {
|
||||
_files -g "*.(json|ftfy)"
|
||||
return $?
|
||||
}
|
||||
|
||||
__fortify_instances() {
|
||||
local -a out
|
||||
shift -p
|
||||
out=( ${(f)"$(_call_program commands fortify ps --short 2>&1)"} )
|
||||
if (( $#out == 0 )); then
|
||||
_message "No active instances"
|
||||
else
|
||||
_describe "active instances" out
|
||||
fi
|
||||
return $?
|
||||
}
|
||||
|
||||
(( $+functions[_fortify_commands] )) || _fortify_commands()
|
||||
{
|
||||
local -a _fortify_cmds
|
||||
_fortify_cmds=(
|
||||
"app:Launch app defined by the specified config file"
|
||||
"run:Configure and start a permissive default sandbox"
|
||||
"show:Show the contents of an app configuration"
|
||||
"ps:List active apps and their state"
|
||||
"version:Show fortify version"
|
||||
"license:Show full license text"
|
||||
"template:Produce a config template"
|
||||
"help:Show help message"
|
||||
)
|
||||
if (( CURRENT == 1 )); then
|
||||
_describe -t commands 'action' _fortify_cmds || compadd "$@"
|
||||
else
|
||||
local curcontext="$curcontext"
|
||||
cmd="${${_fortify_cmds[(r)$words[1]:*]%%:*}}"
|
||||
if (( $+functions[_fortify_$cmd] )); then
|
||||
_fortify_$cmd
|
||||
else
|
||||
_message "no more options"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_arguments -C \
|
||||
'-v[Verbose output]' \
|
||||
'--json[Format output in JSON when applicable]' \
|
||||
'*::fortify command:_fortify_commands'
|
||||
107
container/absolute.go
Normal file
107
container/absolute.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
|
||||
type AbsoluteError struct {
|
||||
Pathname string
|
||||
}
|
||||
|
||||
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
|
||||
func (e *AbsoluteError) Is(target error) bool {
|
||||
var ce *AbsoluteError
|
||||
if !errors.As(target, &ce) {
|
||||
return errors.Is(target, syscall.EINVAL)
|
||||
}
|
||||
return *e == *ce
|
||||
}
|
||||
|
||||
// Absolute holds a pathname checked to be absolute.
|
||||
type Absolute struct {
|
||||
pathname string
|
||||
}
|
||||
|
||||
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
|
||||
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
|
||||
|
||||
func (a *Absolute) String() string {
|
||||
if a.pathname == zeroString {
|
||||
panic("attempted use of zero Absolute")
|
||||
}
|
||||
return a.pathname
|
||||
}
|
||||
|
||||
func (a *Absolute) Is(v *Absolute) bool {
|
||||
if a == nil && v == nil {
|
||||
return true
|
||||
}
|
||||
return a != nil && v != nil &&
|
||||
a.pathname != zeroString && v.pathname != zeroString &&
|
||||
a.pathname == v.pathname
|
||||
}
|
||||
|
||||
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
|
||||
func NewAbs(pathname string) (*Absolute, error) {
|
||||
if !isAbs(pathname) {
|
||||
return nil, &AbsoluteError{pathname}
|
||||
}
|
||||
return &Absolute{pathname}, nil
|
||||
}
|
||||
|
||||
// MustAbs calls [NewAbs] and panics on error.
|
||||
func MustAbs(pathname string) *Absolute {
|
||||
if a, err := NewAbs(pathname); err != nil {
|
||||
panic(err.Error())
|
||||
} else {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
// Append calls [path.Join] with [Absolute] as the first element.
|
||||
func (a *Absolute) Append(elem ...string) *Absolute {
|
||||
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
|
||||
}
|
||||
|
||||
// Dir calls [path.Dir] with [Absolute] as its argument.
|
||||
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
|
||||
|
||||
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
|
||||
func (a *Absolute) GobDecode(data []byte) error {
|
||||
pathname := string(data)
|
||||
if !isAbs(pathname) {
|
||||
return &AbsoluteError{pathname}
|
||||
}
|
||||
a.pathname = pathname
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) }
|
||||
func (a *Absolute) UnmarshalJSON(data []byte) error {
|
||||
var pathname string
|
||||
if err := json.Unmarshal(data, &pathname); err != nil {
|
||||
return err
|
||||
}
|
||||
if !isAbs(pathname) {
|
||||
return &AbsoluteError{pathname}
|
||||
}
|
||||
a.pathname = pathname
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||
func SortAbs(x []*Absolute) {
|
||||
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) })
|
||||
}
|
||||
|
||||
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
|
||||
func CompactAbs(s []*Absolute) []*Absolute {
|
||||
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() })
|
||||
}
|
||||
348
container/absolute_test.go
Normal file
348
container/absolute_test.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAbsoluteError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
err error
|
||||
cmp error
|
||||
ok bool
|
||||
}{
|
||||
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
|
||||
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
|
||||
{"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
|
||||
{"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if got := errors.Is(tc.err, tc.cmp); got != tc.ok {
|
||||
t.Errorf("Is: %v, want %v", got, tc.ok)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("string", func(t *testing.T) {
|
||||
want := `path "etc" is not absolute`
|
||||
if got := (&AbsoluteError{"etc"}).Error(); got != want {
|
||||
t.Errorf("Error: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewAbs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
pathname string
|
||||
want *Absolute
|
||||
wantErr error
|
||||
}{
|
||||
{"good", "/etc", MustAbs("/etc"), nil},
|
||||
{"not absolute", "etc", nil, &AbsoluteError{"etc"}},
|
||||
{"zero", "", nil, &AbsoluteError{""}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := NewAbs(tc.pathname)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
|
||||
}
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("NewAbs: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("must", func(t *testing.T) {
|
||||
defer func() {
|
||||
wantPanic := `path "etc" is not absolute`
|
||||
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
|
||||
MustAbs("etc")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAbsoluteString(t *testing.T) {
|
||||
t.Run("passthrough", func(t *testing.T) {
|
||||
pathname := "/etc"
|
||||
if got := (&Absolute{pathname}).String(); got != pathname {
|
||||
t.Errorf("String: %q, want %q", got, pathname)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero", func(t *testing.T) {
|
||||
defer func() {
|
||||
wantPanic := "attempted use of zero Absolute"
|
||||
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("String: panic = %v, want %v", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
|
||||
panic(new(Absolute).String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAbsoluteIs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
a, v *Absolute
|
||||
want bool
|
||||
}{
|
||||
{"nil", (*Absolute)(nil), (*Absolute)(nil), true},
|
||||
{"nil a", (*Absolute)(nil), MustAbs("/"), false},
|
||||
{"nil v", MustAbs("/"), (*Absolute)(nil), false},
|
||||
{"zero", new(Absolute), new(Absolute), false},
|
||||
{"zero a", new(Absolute), MustAbs("/"), false},
|
||||
{"zero v", MustAbs("/"), new(Absolute), false},
|
||||
{"equals", MustAbs("/"), MustAbs("/"), true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.a.Is(tc.v); got != tc.want {
|
||||
t.Errorf("Is: %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type sCheck struct {
|
||||
Pathname *Absolute `json:"val"`
|
||||
Magic int `json:"magic"`
|
||||
}
|
||||
|
||||
func TestCodecAbsolute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
a *Absolute
|
||||
|
||||
wantErr error
|
||||
|
||||
gob, sGob string
|
||||
json, sJson string
|
||||
}{
|
||||
{"nil", nil, nil,
|
||||
"\x00", "\x00",
|
||||
`null`, `{"val":null,"magic":3236757504}`},
|
||||
|
||||
{"good", MustAbs("/etc"),
|
||||
nil,
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x10\xff\x84\x01\x04/etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
|
||||
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
||||
{"not absolute", nil,
|
||||
&AbsoluteError{"etc"},
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
|
||||
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
||||
{"zero", nil,
|
||||
new(AbsoluteError),
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
`""`, `{"val":"","magic":3236757504}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run("gob", func(t *testing.T) {
|
||||
if tc.gob == "\x00" && tc.sGob == "\x00" {
|
||||
// these values mark the current test to skip gob
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("encode", func(t *testing.T) {
|
||||
// encode is unchecked
|
||||
if errors.Is(tc.wantErr, syscall.EINVAL) {
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
buf := new(bytes.Buffer)
|
||||
err := gob.NewEncoder(buf).Encode(tc.a)
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Encode: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != nil {
|
||||
goto checkSEncode
|
||||
}
|
||||
if buf.String() != tc.gob {
|
||||
t.Errorf("Encode:\n%q\nwant:\n%q", buf.String(), tc.gob)
|
||||
}
|
||||
}
|
||||
|
||||
checkSEncode:
|
||||
{
|
||||
buf := new(bytes.Buffer)
|
||||
err := gob.NewEncoder(buf).Encode(&sCheck{tc.a, syscall.MS_MGC_VAL})
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Encode: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != nil {
|
||||
return
|
||||
}
|
||||
if buf.String() != tc.sGob {
|
||||
t.Errorf("Encode:\n%q\nwant:\n%q", buf.String(), tc.sGob)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
{
|
||||
var gotA *Absolute
|
||||
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Decode: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != nil {
|
||||
goto checkSDecode
|
||||
}
|
||||
if !reflect.DeepEqual(tc.a, gotA) {
|
||||
t.Errorf("Decode: %#v, want %#v", tc.a, gotA)
|
||||
}
|
||||
}
|
||||
|
||||
checkSDecode:
|
||||
{
|
||||
var gotSCheck sCheck
|
||||
err := gob.NewDecoder(strings.NewReader(tc.sGob)).Decode(&gotSCheck)
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Decode: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != nil {
|
||||
return
|
||||
}
|
||||
want := sCheck{tc.a, syscall.MS_MGC_VAL}
|
||||
if !reflect.DeepEqual(gotSCheck, want) {
|
||||
t.Errorf("Decode: %#v, want %#v", gotSCheck, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
t.Run("marshal", func(t *testing.T) {
|
||||
// marshal is unchecked
|
||||
if errors.Is(tc.wantErr, syscall.EINVAL) {
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
d, err := json.Marshal(tc.a)
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Marshal: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != nil {
|
||||
goto checkSMarshal
|
||||
}
|
||||
if string(d) != tc.json {
|
||||
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.json)
|
||||
}
|
||||
}
|
||||
|
||||
checkSMarshal:
|
||||
{
|
||||
d, err := json.Marshal(&sCheck{tc.a, syscall.MS_MGC_VAL})
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Marshal: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != nil {
|
||||
return
|
||||
}
|
||||
if string(d) != tc.sJson {
|
||||
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.sJson)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unmarshal", func(t *testing.T) {
|
||||
{
|
||||
var gotA *Absolute
|
||||
err := json.Unmarshal([]byte(tc.json), &gotA)
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != nil {
|
||||
goto checkSUnmarshal
|
||||
}
|
||||
if !reflect.DeepEqual(tc.a, gotA) {
|
||||
t.Errorf("Unmarshal: %#v, want %#v", tc.a, gotA)
|
||||
}
|
||||
}
|
||||
|
||||
checkSUnmarshal:
|
||||
{
|
||||
var gotSCheck sCheck
|
||||
err := json.Unmarshal([]byte(tc.sJson), &gotSCheck)
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != nil {
|
||||
return
|
||||
}
|
||||
want := sCheck{tc.a, syscall.MS_MGC_VAL}
|
||||
if !reflect.DeepEqual(gotSCheck, want) {
|
||||
t.Errorf("Unmarshal: %#v, want %#v", gotSCheck, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("json passthrough", func(t *testing.T) {
|
||||
wantErr := "invalid character ':' looking for beginning of value"
|
||||
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
|
||||
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAbsoluteWrap(t *testing.T) {
|
||||
t.Run("join", func(t *testing.T) {
|
||||
want := "/etc/nix/nix.conf"
|
||||
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
|
||||
t.Errorf("Append: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dir", func(t *testing.T) {
|
||||
want := "/"
|
||||
if got := MustAbs("/etc").Dir(); got.String() != want {
|
||||
t.Errorf("Dir: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sort", func(t *testing.T) {
|
||||
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
|
||||
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
|
||||
SortAbs(got)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("SortAbs: %#v, want %#v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("compact", func(t *testing.T) {
|
||||
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
|
||||
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("CompactAbs: %#v, want %#v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
69
container/autoetc.go
Normal file
69
container/autoetc.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(AutoEtcOp)) }
|
||||
|
||||
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||
func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
|
||||
e := &AutoEtcOp{prefix}
|
||||
f.Mkdir(AbsFHSEtc, 0755)
|
||||
f.Bind(host, e.hostPath(), 0)
|
||||
*f = append(*f, e)
|
||||
return f
|
||||
}
|
||||
|
||||
type AutoEtcOp struct{ Prefix string }
|
||||
|
||||
func (e *AutoEtcOp) Valid() bool { return e != nil }
|
||||
func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
if state.nonrepeatable&nrAutoEtc != 0 {
|
||||
return msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
|
||||
}
|
||||
state.nonrepeatable |= nrAutoEtc
|
||||
|
||||
const target = sysrootPath + FHSEtc
|
||||
rel := e.hostRel() + "/"
|
||||
|
||||
if err := k.mkdirAll(target, 0755); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
for _, ent := range d {
|
||||
n := ent.Name()
|
||||
switch n {
|
||||
case ".host", "passwd", "group":
|
||||
|
||||
case "mtab":
|
||||
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
|
||||
default:
|
||||
if err = k.symlink(rel+n, target+n); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *AutoEtcOp) hostPath() *Absolute { return AbsFHSEtc.Append(e.hostRel()) }
|
||||
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
||||
|
||||
func (e *AutoEtcOp) Is(op Op) bool {
|
||||
ve, ok := op.(*AutoEtcOp)
|
||||
return ok && e.Valid() && ve.Valid() && *e == *ve
|
||||
}
|
||||
func (*AutoEtcOp) prefix() string { return "setting up" }
|
||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||
291
container/autoetc_test.go
Normal file
291
container/autoetc_test.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAutoEtcOp(t *testing.T) {
|
||||
t.Run("nonrepeatable", func(t *testing.T) {
|
||||
wantErr := msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
|
||||
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
|
||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"mkdirAll", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"readdir", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"symlink", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||
"modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf",
|
||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"symlink mtab", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||
"modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf",
|
||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"success nested", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||
"modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf",
|
||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
|
||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||
"modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf",
|
||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*AutoEtcOp)(nil), false},
|
||||
{"zero", new(AutoEtcOp), true},
|
||||
{"populated", &AutoEtcOp{Prefix: ":3"}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
|
||||
&MkdirOp{Path: MustAbs("/etc/"), Perm: 0755},
|
||||
&BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
},
|
||||
&AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(AutoEtcOp), new(AutoEtcOp), true},
|
||||
{"differs", &AutoEtcOp{Prefix: "\x00"}, &AutoEtcOp{":3"}, false},
|
||||
{"equals", &AutoEtcOp{Prefix: ":3"}, &AutoEtcOp{":3"}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"etc", &AutoEtcOp{
|
||||
Prefix: ":3",
|
||||
}, "setting up", "auto etc :3"},
|
||||
})
|
||||
|
||||
t.Run("host path rel", func(t *testing.T) {
|
||||
op := &AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"}
|
||||
wantHostPath := "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"
|
||||
wantHostRel := ".host/048090b6ed8f9ebb10e275ff5d8c0659"
|
||||
if got := op.hostPath(); got.String() != wantHostPath {
|
||||
t.Errorf("hostPath: %q, want %q", got, wantHostPath)
|
||||
}
|
||||
if got := op.hostRel(); got != wantHostRel {
|
||||
t.Errorf("hostRel: %q, want %q", got, wantHostRel)
|
||||
}
|
||||
})
|
||||
}
|
||||
93
container/autoroot.go
Normal file
93
container/autoroot.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(AutoRootOp)) }
|
||||
|
||||
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root.
|
||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||
func (f *Ops) Root(host *Absolute, flags int) *Ops {
|
||||
*f = append(*f, &AutoRootOp{host, flags, nil})
|
||||
return f
|
||||
}
|
||||
|
||||
type AutoRootOp struct {
|
||||
Host *Absolute
|
||||
// passed through to bindMount
|
||||
Flags int
|
||||
|
||||
// obtained during early;
|
||||
// these wrap the underlying Op because BindMountOp is relatively complex,
|
||||
// so duplicating that code would be unwise
|
||||
resolved []Op
|
||||
}
|
||||
|
||||
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
|
||||
|
||||
func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
|
||||
if d, err := k.readdir(r.Host.String()); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
r.resolved = make([]Op, 0, len(d))
|
||||
for _, ent := range d {
|
||||
name := ent.Name()
|
||||
if IsAutoRootBindable(name) {
|
||||
op := &BindMountOp{
|
||||
Source: r.Host.Append(name),
|
||||
Target: AbsFHSRoot.Append(name),
|
||||
Flags: r.Flags,
|
||||
}
|
||||
if err = op.early(state, k); err != nil {
|
||||
return err
|
||||
}
|
||||
r.resolved = append(r.resolved, op)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
if state.nonrepeatable&nrAutoRoot != 0 {
|
||||
return msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
|
||||
}
|
||||
state.nonrepeatable |= nrAutoRoot
|
||||
|
||||
for _, op := range r.resolved {
|
||||
k.verbosef("%s %s", op.prefix(), op)
|
||||
if err := op.apply(state, k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AutoRootOp) Is(op Op) bool {
|
||||
vr, ok := op.(*AutoRootOp)
|
||||
return ok && r.Valid() && vr.Valid() &&
|
||||
r.Host.Is(vr.Host) &&
|
||||
r.Flags == vr.Flags
|
||||
}
|
||||
func (*AutoRootOp) prefix() string { return "setting up" }
|
||||
func (r *AutoRootOp) String() string {
|
||||
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
|
||||
}
|
||||
|
||||
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
|
||||
func IsAutoRootBindable(name string) bool {
|
||||
switch name {
|
||||
case "proc", "dev", "tmp", "mnt", "etc":
|
||||
|
||||
case "": // guard against accidentally binding /
|
||||
// should be unreachable
|
||||
msg.Verbose("got unexpected root entry")
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
200
container/autoroot_test.go
Normal file
200
container/autoroot_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAutoRootOp(t *testing.T) {
|
||||
t.Run("nonrepeatable", func(t *testing.T) {
|
||||
wantErr := msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
|
||||
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
|
||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/"}, stubDir(), errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
|
||||
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
||||
{"evalSymlinks", expectArgs{"/bin"}, "", errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
|
||||
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
||||
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
|
||||
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
|
||||
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
|
||||
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
|
||||
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
|
||||
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
|
||||
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
|
||||
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
|
||||
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
|
||||
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
|
||||
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
|
||||
}, nil, []kexpect{
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil},
|
||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(false), errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
||||
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
|
||||
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
|
||||
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
|
||||
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
|
||||
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
|
||||
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
|
||||
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
|
||||
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
|
||||
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
|
||||
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
|
||||
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
|
||||
}, nil, []kexpect{
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil},
|
||||
}, nil, []kexpect{
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr/bin"), MustAbs("/var/lib/planterette/base/debian:f92c9052/bin"), MustAbs("/bin"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/home"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/lib64"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/lost+found"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/nix"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/root"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/run"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/srv"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/sys"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/usr"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*AutoRootOp)(nil), false},
|
||||
{"zero", new(AutoRootOp), false},
|
||||
{"valid", &AutoRootOp{Host: MustAbs("/")}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"pd", new(Ops).Root(MustAbs("/"), BindWritable), Ops{
|
||||
&AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(AutoRootOp), new(AutoRootOp), false},
|
||||
|
||||
{"internal ne", &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
resolved: []Op{new(BindMountOp)},
|
||||
}, true},
|
||||
|
||||
{"flags differs", &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable | BindDevice,
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, false},
|
||||
|
||||
{"host differs", &AutoRootOp{
|
||||
Host: MustAbs("/tmp/"),
|
||||
Flags: BindWritable,
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, false},
|
||||
|
||||
{"equals", &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"root", &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, "setting up", `auto root "/" flags 0x2`},
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsAutoRootBindable(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{"proc", false},
|
||||
{"dev", false},
|
||||
{"tmp", false},
|
||||
{"mnt", false},
|
||||
{"etc", false},
|
||||
{"", false},
|
||||
|
||||
{"var", true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := IsAutoRootBindable(tc.name); got != tc.want {
|
||||
t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
89
container/capability.go
Normal file
89
container/capability.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
||||
|
||||
PR_CAP_AMBIENT = 0x2f
|
||||
PR_CAP_AMBIENT_RAISE = 0x2
|
||||
PR_CAP_AMBIENT_CLEAR_ALL = 0x4
|
||||
|
||||
CAP_SYS_ADMIN = 0x15
|
||||
CAP_SETPCAP = 0x8
|
||||
CAP_DAC_OVERRIDE = 0x1
|
||||
)
|
||||
|
||||
type (
|
||||
capHeader struct {
|
||||
version uint32
|
||||
pid int32
|
||||
}
|
||||
|
||||
capData struct {
|
||||
effective uint32
|
||||
permitted uint32
|
||||
inheritable uint32
|
||||
}
|
||||
)
|
||||
|
||||
// See CAP_TO_INDEX in linux/capability.h:
|
||||
func capToIndex(cap uintptr) uintptr { return cap >> 5 }
|
||||
|
||||
// See CAP_TO_MASK in linux/capability.h:
|
||||
func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) }
|
||||
|
||||
func capset(hdrp *capHeader, datap *[2]capData) error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
syscall.SYS_CAPSET,
|
||||
uintptr(unsafe.Pointer(hdrp)),
|
||||
uintptr(unsafe.Pointer(&datap[0])), 0,
|
||||
)
|
||||
if r != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
|
||||
func capBoundingSetDrop(cap uintptr) error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
syscall.SYS_PRCTL,
|
||||
syscall.PR_CAPBSET_DROP,
|
||||
cap, 0,
|
||||
)
|
||||
if r != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// capAmbientClearAll clears the ambient capability set of the calling thread.
|
||||
func capAmbientClearAll() error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
syscall.SYS_PRCTL,
|
||||
PR_CAP_AMBIENT,
|
||||
PR_CAP_AMBIENT_CLEAR_ALL, 0,
|
||||
)
|
||||
if r != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// capAmbientRaise adds to the ambient capability set of the calling thread.
|
||||
func capAmbientRaise(cap uintptr) error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
syscall.SYS_PRCTL,
|
||||
PR_CAP_AMBIENT,
|
||||
PR_CAP_AMBIENT_RAISE,
|
||||
cap,
|
||||
)
|
||||
if r != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
41
container/capability_test.go
Normal file
41
container/capability_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package container
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCapToIndex(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cap uintptr
|
||||
want uintptr
|
||||
}{
|
||||
{"CAP_SYS_ADMIN", CAP_SYS_ADMIN, 0},
|
||||
{"CAP_SETPCAP", CAP_SETPCAP, 0},
|
||||
{"CAP_DAC_OVERRIDE", CAP_DAC_OVERRIDE, 0},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := capToIndex(tc.cap); got != tc.want {
|
||||
t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapToMask(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cap uintptr
|
||||
want uint32
|
||||
}{
|
||||
{"CAP_SYS_ADMIN", CAP_SYS_ADMIN, 0x200000},
|
||||
{"CAP_SETPCAP", CAP_SETPCAP, 0x100},
|
||||
{"CAP_DAC_OVERRIDE", CAP_DAC_OVERRIDE, 0x2},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := capToMask(tc.cap); got != tc.want {
|
||||
t.Errorf("capToMask: %#x, want %#x", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
324
container/container.go
Normal file
324
container/container.go
Normal file
@@ -0,0 +1,324 @@
|
||||
// Package container implements unprivileged Linux containers with built-in support for syscall filtering.
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
const (
|
||||
// CancelSignal is the signal expected by container init on context cancel.
|
||||
// A custom [Container.Cancel] function must eventually deliver this signal.
|
||||
CancelSignal = SIGTERM
|
||||
)
|
||||
|
||||
type (
|
||||
// Container represents a container environment being prepared or run.
|
||||
// None of [Container] methods are safe for concurrent use.
|
||||
Container struct {
|
||||
// Cgroup fd, nil to disable.
|
||||
Cgroup *int
|
||||
// ExtraFiles passed through to initial process in the container,
|
||||
// with behaviour identical to its [exec.Cmd] counterpart.
|
||||
ExtraFiles []*os.File
|
||||
|
||||
// param encoder for shim and init
|
||||
setup *gob.Encoder
|
||||
// cancels cmd
|
||||
cancel context.CancelFunc
|
||||
// closed after Wait returns
|
||||
wait chan struct{}
|
||||
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
Cancel func(cmd *exec.Cmd) error
|
||||
WaitDelay time.Duration
|
||||
|
||||
cmd *exec.Cmd
|
||||
ctx context.Context
|
||||
Params
|
||||
}
|
||||
|
||||
// Params holds container configuration and is safe to serialise.
|
||||
Params struct {
|
||||
// Working directory in the container.
|
||||
Dir *Absolute
|
||||
// Initial process environment.
|
||||
Env []string
|
||||
// Pathname of initial process in the container.
|
||||
Path *Absolute
|
||||
// Initial process argv.
|
||||
Args []string
|
||||
// Deliver SIGINT to the initial process on context cancellation.
|
||||
ForwardCancel bool
|
||||
// time to wait for linger processes after death of initial process
|
||||
AdoptWaitDelay time.Duration
|
||||
|
||||
// Mapped Uid in user namespace.
|
||||
Uid int
|
||||
// Mapped Gid in user namespace.
|
||||
Gid int
|
||||
// Hostname value in UTS namespace.
|
||||
Hostname string
|
||||
// Sequential container setup ops.
|
||||
*Ops
|
||||
|
||||
// Seccomp system call filter rules.
|
||||
SeccompRules []seccomp.NativeRule
|
||||
// Extra seccomp flags.
|
||||
SeccompFlags seccomp.ExportFlag
|
||||
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
|
||||
SeccompPresets seccomp.FilterPreset
|
||||
// Do not load seccomp program.
|
||||
SeccompDisable bool
|
||||
|
||||
// Permission bits of newly created parent directories.
|
||||
// The zero value is interpreted as 0755.
|
||||
ParentPerm os.FileMode
|
||||
// Do not syscall.Setsid.
|
||||
RetainSession bool
|
||||
// Do not [syscall.CLONE_NEWNET].
|
||||
HostNet bool
|
||||
// Do not [LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET].
|
||||
HostAbstract bool
|
||||
// Retain CAP_SYS_ADMIN.
|
||||
Privileged bool
|
||||
}
|
||||
)
|
||||
|
||||
// Start starts the container init. The init process blocks until Serve is called.
|
||||
func (p *Container) Start() error {
|
||||
if p.cmd != nil {
|
||||
return errors.New("container: already started")
|
||||
}
|
||||
if p.Ops == nil || len(*p.Ops) == 0 {
|
||||
return errors.New("container: starting an empty container")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(p.ctx)
|
||||
p.cancel = cancel
|
||||
|
||||
// map to overflow id to work around ownership checks
|
||||
if p.Uid < 1 {
|
||||
p.Uid = OverflowUid()
|
||||
}
|
||||
if p.Gid < 1 {
|
||||
p.Gid = OverflowGid()
|
||||
}
|
||||
|
||||
if !p.RetainSession {
|
||||
p.SeccompPresets |= seccomp.PresetDenyTTY
|
||||
}
|
||||
|
||||
if p.AdoptWaitDelay == 0 {
|
||||
p.AdoptWaitDelay = 5 * time.Second
|
||||
}
|
||||
// to allow disabling this behaviour
|
||||
if p.AdoptWaitDelay < 0 {
|
||||
p.AdoptWaitDelay = 0
|
||||
}
|
||||
|
||||
p.cmd = exec.CommandContext(ctx, MustExecutable())
|
||||
p.cmd.Args = []string{initName}
|
||||
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
|
||||
p.cmd.WaitDelay = p.WaitDelay
|
||||
if p.Cancel != nil {
|
||||
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
||||
} else {
|
||||
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
|
||||
}
|
||||
p.cmd.Dir = FHSRoot
|
||||
p.cmd.SysProcAttr = &SysProcAttr{
|
||||
Setsid: !p.RetainSession,
|
||||
Pdeathsig: SIGKILL,
|
||||
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
||||
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
||||
|
||||
AmbientCaps: []uintptr{
|
||||
// general container setup
|
||||
CAP_SYS_ADMIN,
|
||||
// drop capabilities
|
||||
CAP_SETPCAP,
|
||||
// overlay access to upperdir and workdir
|
||||
CAP_DAC_OVERRIDE,
|
||||
},
|
||||
|
||||
UseCgroupFD: p.Cgroup != nil,
|
||||
}
|
||||
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||
}
|
||||
if !p.HostNet {
|
||||
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
|
||||
}
|
||||
|
||||
// place setup pipe before user supplied extra files, this is later restored by init
|
||||
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot create shim setup pipe:")
|
||||
} else {
|
||||
p.setup = e
|
||||
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||
}
|
||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
p.wait = make(chan struct{})
|
||||
|
||||
done <- func() error { // setup depending on per-thread state must happen here
|
||||
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
|
||||
if err := SetNoNewPrivs(); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"prctl(PR_SET_NO_NEW_PRIVS):")
|
||||
}
|
||||
|
||||
// landlock: depends on per-thread state but acts on a process group
|
||||
{
|
||||
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
|
||||
if !p.HostAbstract {
|
||||
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
}
|
||||
|
||||
if abi, err := LandlockGetABI(); err != nil {
|
||||
if p.HostAbstract {
|
||||
// landlock can be skipped here as it restricts access to resources
|
||||
// already covered by namespaces (pid)
|
||||
goto landlockOut
|
||||
}
|
||||
return wrapErrSuffix(err,
|
||||
"landlock does not appear to be enabled:")
|
||||
} else if abi < 6 {
|
||||
if p.HostAbstract {
|
||||
// see above comment
|
||||
goto landlockOut
|
||||
}
|
||||
return msg.WrapErr(ENOSYS,
|
||||
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
|
||||
} else {
|
||||
msg.Verbosef("landlock abi version %d", abi)
|
||||
}
|
||||
|
||||
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot create landlock ruleset:")
|
||||
} else {
|
||||
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
||||
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
||||
_ = Close(rulesetFd)
|
||||
return wrapErrSuffix(err,
|
||||
"cannot enforce landlock ruleset:")
|
||||
}
|
||||
if err = Close(rulesetFd); err != nil {
|
||||
msg.Verbosef("cannot close landlock ruleset: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
}
|
||||
|
||||
landlockOut:
|
||||
}
|
||||
|
||||
msg.Verbose("starting container init")
|
||||
if err := p.cmd.Start(); err != nil {
|
||||
return msg.WrapErr(err, err.Error())
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
// keep this thread alive until Wait returns for cancel
|
||||
<-p.wait
|
||||
}()
|
||||
return <-done
|
||||
}
|
||||
|
||||
// Serve serves [Container.Params] to the container init.
|
||||
// Serve must only be called once.
|
||||
func (p *Container) Serve() error {
|
||||
if p.setup == nil {
|
||||
panic("invalid serve")
|
||||
}
|
||||
|
||||
setup := p.setup
|
||||
p.setup = nil
|
||||
|
||||
if p.Path == nil {
|
||||
p.cancel()
|
||||
return msg.WrapErr(EINVAL, "invalid executable pathname")
|
||||
}
|
||||
|
||||
// do not transmit nil
|
||||
if p.Dir == nil {
|
||||
p.Dir = AbsFHSRoot
|
||||
}
|
||||
if p.SeccompRules == nil {
|
||||
p.SeccompRules = make([]seccomp.NativeRule, 0)
|
||||
}
|
||||
|
||||
err := setup.Encode(
|
||||
&initParams{
|
||||
p.Params,
|
||||
Getuid(),
|
||||
Getgid(),
|
||||
len(p.ExtraFiles),
|
||||
msg.IsVerbose(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
p.cancel()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
|
||||
func (p *Container) Wait() error {
|
||||
if p.cmd == nil {
|
||||
return EINVAL
|
||||
}
|
||||
|
||||
err := p.cmd.Wait()
|
||||
p.cancel()
|
||||
if p.wait != nil && err == nil {
|
||||
close(p.wait)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Container) String() string {
|
||||
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
|
||||
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
|
||||
}
|
||||
|
||||
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd].
|
||||
func (p *Container) ProcessState() *os.ProcessState {
|
||||
if p.cmd == nil {
|
||||
return nil
|
||||
}
|
||||
return p.cmd.ProcessState
|
||||
}
|
||||
|
||||
// New returns the address to a new instance of [Container] that requires further initialisation before use.
|
||||
func New(ctx context.Context) *Container {
|
||||
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
|
||||
}
|
||||
|
||||
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
|
||||
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
|
||||
z := New(ctx)
|
||||
z.Path = pathname
|
||||
z.Args = append([]string{name}, args...)
|
||||
return z
|
||||
}
|
||||
574
container/container_test.go
Normal file
574
container/container_test.go
Normal file
@@ -0,0 +1,574 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/vfs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/ldd"
|
||||
)
|
||||
|
||||
const (
|
||||
ignore = "\x00"
|
||||
ignoreV = -1
|
||||
|
||||
pathPrefix = "/etc/hakurei/"
|
||||
pathWantMnt = pathPrefix + "want-mnt"
|
||||
pathReadonly = pathPrefix + "readonly"
|
||||
)
|
||||
|
||||
type testVal any
|
||||
|
||||
func emptyOps(t *testing.T) (*container.Ops, context.Context) { return new(container.Ops), t.Context() }
|
||||
func earlyOps(ops *container.Ops) func(t *testing.T) (*container.Ops, context.Context) {
|
||||
return func(t *testing.T) (*container.Ops, context.Context) { return ops, t.Context() }
|
||||
}
|
||||
|
||||
func emptyMnt(*testing.T, context.Context) []*vfs.MountInfoEntry { return nil }
|
||||
func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*vfs.MountInfoEntry {
|
||||
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
|
||||
}
|
||||
|
||||
var containerTestCases = []struct {
|
||||
name string
|
||||
filter bool
|
||||
session bool
|
||||
net bool
|
||||
ro bool
|
||||
|
||||
ops func(t *testing.T) (*container.Ops, context.Context)
|
||||
mnt func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry
|
||||
|
||||
uid int
|
||||
gid int
|
||||
|
||||
rules []seccomp.NativeRule
|
||||
flags seccomp.ExportFlag
|
||||
presets seccomp.FilterPreset
|
||||
}{
|
||||
{"minimal", true, false, false, true,
|
||||
emptyOps, emptyMnt,
|
||||
1000, 100, nil, 0, seccomp.PresetStrict},
|
||||
{"allow", true, true, true, false,
|
||||
emptyOps, emptyMnt,
|
||||
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
|
||||
{"no filter", false, true, true, true,
|
||||
emptyOps, emptyMnt,
|
||||
1000, 100, nil, 0, seccomp.PresetExt},
|
||||
{"custom rules", true, true, true, false,
|
||||
emptyOps, emptyMnt,
|
||||
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
|
||||
|
||||
{"tmpfs", true, false, false, true,
|
||||
earlyOps(new(container.Ops).
|
||||
Tmpfs(hst.AbsTmp, 0, 0755),
|
||||
),
|
||||
earlyMnt(
|
||||
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
|
||||
),
|
||||
9, 9, nil, 0, seccomp.PresetStrict},
|
||||
|
||||
{"dev", true, true /* go test output is not a tty */, false, false,
|
||||
earlyOps(new(container.Ops).
|
||||
Dev(container.MustAbs("/dev"), true),
|
||||
),
|
||||
earlyMnt(
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
|
||||
),
|
||||
1971, 100, nil, 0, seccomp.PresetStrict},
|
||||
|
||||
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
|
||||
earlyOps(new(container.Ops).
|
||||
Dev(container.MustAbs("/dev"), false),
|
||||
),
|
||||
earlyMnt(
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||
),
|
||||
1971, 100, nil, 0, seccomp.PresetStrict},
|
||||
|
||||
{"overlay", true, false, false, true,
|
||||
func(t *testing.T) (*container.Ops, context.Context) {
|
||||
tempDir := container.MustAbs(t.TempDir())
|
||||
lower0, lower1, upper, work :=
|
||||
tempDir.Append("lower0"),
|
||||
tempDir.Append("lower1"),
|
||||
tempDir.Append("upper"),
|
||||
tempDir.Append("work")
|
||||
for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
|
||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||
t.Fatalf("Mkdir: error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return new(container.Ops).
|
||||
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
|
||||
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
|
||||
testVal("lower1"), lower1),
|
||||
testVal("lower0"), lower0),
|
||||
testVal("work"), work),
|
||||
testVal("upper"), upper)
|
||||
},
|
||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||
return []*vfs.MountInfoEntry{
|
||||
ent("/", hst.Tmp, "rw", "overlay", "overlay",
|
||||
"rw,lowerdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
|
||||
",upperdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
|
||||
",workdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
|
||||
",redirect_dir=nofollow,uuid=on,userxattr"),
|
||||
}
|
||||
},
|
||||
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||
|
||||
{"overlay ephemeral", true, false, false, true,
|
||||
func(t *testing.T) (*container.Ops, context.Context) {
|
||||
tempDir := container.MustAbs(t.TempDir())
|
||||
lower0, lower1 :=
|
||||
tempDir.Append("lower0"),
|
||||
tempDir.Append("lower1")
|
||||
for _, a := range []*container.Absolute{lower0, lower1} {
|
||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||
t.Fatalf("Mkdir: error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return new(container.Ops).
|
||||
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
|
||||
t.Context()
|
||||
},
|
||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||
return []*vfs.MountInfoEntry{
|
||||
// contains random suffix
|
||||
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
|
||||
}
|
||||
},
|
||||
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||
|
||||
{"overlay readonly", true, false, false, true,
|
||||
func(t *testing.T) (*container.Ops, context.Context) {
|
||||
tempDir := container.MustAbs(t.TempDir())
|
||||
lower0, lower1 :=
|
||||
tempDir.Append("lower0"),
|
||||
tempDir.Append("lower1")
|
||||
for _, a := range []*container.Absolute{lower0, lower1} {
|
||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||
t.Fatalf("Mkdir: error = %v", err)
|
||||
}
|
||||
}
|
||||
return new(container.Ops).
|
||||
OverlayReadonly(hst.AbsTmp, lower0, lower1),
|
||||
context.WithValue(context.WithValue(t.Context(),
|
||||
testVal("lower1"), lower1),
|
||||
testVal("lower0"), lower0)
|
||||
},
|
||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||
return []*vfs.MountInfoEntry{
|
||||
ent("/", hst.Tmp, "rw", "overlay", "overlay",
|
||||
"ro,lowerdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
|
||||
",redirect_dir=nofollow,userxattr"),
|
||||
}
|
||||
},
|
||||
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||
}
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
replaceOutput(t)
|
||||
|
||||
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
|
||||
wantErr := context.Canceled
|
||||
wantExitCode := 0
|
||||
if err := c.Wait(); !errors.Is(err, wantErr) {
|
||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
||||
t.Errorf("Wait: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if ps := c.ProcessState(); ps == nil {
|
||||
t.Errorf("ProcessState unexpectedly returned nil")
|
||||
} else if code := ps.ExitCode(); code != wantExitCode {
|
||||
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
|
||||
}
|
||||
}))
|
||||
|
||||
t.Run("forward", testContainerCancel(func(c *container.Container) {
|
||||
c.ForwardCancel = true
|
||||
}, func(t *testing.T, c *container.Container) {
|
||||
var exitError *exec.ExitError
|
||||
if err := c.Wait(); !errors.As(err, &exitError) {
|
||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
||||
t.Errorf("Wait: error = %v", err)
|
||||
}
|
||||
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
||||
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
|
||||
}
|
||||
}))
|
||||
|
||||
for i, tc := range containerTestCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wantOps, wantOpsCtx := tc.ops(t)
|
||||
wantMnt := tc.mnt(t, wantOpsCtx)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
var libPaths []*container.Absolute
|
||||
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
|
||||
c.Uid = tc.uid
|
||||
c.Gid = tc.gid
|
||||
c.Hostname = hostnameFromTestCase(tc.name)
|
||||
output := new(bytes.Buffer)
|
||||
if !testing.Verbose() {
|
||||
c.Stdout, c.Stderr = output, output
|
||||
} else {
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
}
|
||||
c.WaitDelay = helperDefaultTimeout
|
||||
*c.Ops = append(*c.Ops, *wantOps...)
|
||||
c.SeccompRules = tc.rules
|
||||
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
|
||||
c.SeccompPresets = tc.presets
|
||||
c.SeccompDisable = !tc.filter
|
||||
c.RetainSession = tc.session
|
||||
c.HostNet = tc.net
|
||||
|
||||
c.
|
||||
Readonly(container.MustAbs(pathReadonly), 0755).
|
||||
Tmpfs(container.MustAbs("/tmp"), 0, 0755).
|
||||
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
|
||||
// needs /proc to check mountinfo
|
||||
c.Proc(container.MustAbs("/proc"))
|
||||
|
||||
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
|
||||
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
|
||||
mnt = append(mnt,
|
||||
ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
|
||||
// Bind(os.Args[0], helperInnerPath, 0)
|
||||
ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
|
||||
)
|
||||
for _, a := range libPaths {
|
||||
// Bind(name, name, 0)
|
||||
mnt = append(mnt, ent(ignore, a.String(), "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
|
||||
}
|
||||
mnt = append(mnt, wantMnt...)
|
||||
mnt = append(mnt,
|
||||
// Readonly(pathReadonly, 0755)
|
||||
ent("/", pathReadonly, "ro,nosuid,nodev", "tmpfs", "readonly", ignore),
|
||||
// Tmpfs("/tmp", 0, 0755)
|
||||
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
|
||||
// Place("/etc/hostname", []byte(hostname))
|
||||
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
|
||||
// Proc("/proc")
|
||||
ent("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"),
|
||||
// Place(pathWantMnt, want.Bytes())
|
||||
ent(ignore, pathWantMnt, "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
|
||||
)
|
||||
want := new(bytes.Buffer)
|
||||
if err := gob.NewEncoder(want).Encode(mnt); err != nil {
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
t.Fatalf("cannot serialise expected mount points: %v", err)
|
||||
}
|
||||
c.Place(container.MustAbs(pathWantMnt), want.Bytes())
|
||||
|
||||
if tc.ro {
|
||||
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
|
||||
}
|
||||
|
||||
if err := c.Start(); err != nil {
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
container.GetOutput().PrintBaseErr(err, "start:")
|
||||
t.Fatalf("cannot start container: %v", err)
|
||||
} else if err = c.Serve(); err != nil {
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
container.GetOutput().PrintBaseErr(err, "serve:")
|
||||
t.Errorf("cannot serve setup params: %v", err)
|
||||
}
|
||||
if err := c.Wait(); err != nil {
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
||||
t.Fatalf("wait: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
|
||||
return &vfs.MountInfoEntry{
|
||||
ID: ignoreV,
|
||||
Parent: ignoreV,
|
||||
Devno: vfs.DevT{ignoreV, ignoreV},
|
||||
Root: root,
|
||||
Target: target,
|
||||
VfsOptstr: vfsOptstr,
|
||||
OptFields: []string{ignore},
|
||||
FsType: fsType,
|
||||
Source: source,
|
||||
FsOptstr: fsOptstr,
|
||||
}
|
||||
}
|
||||
|
||||
func hostnameFromTestCase(name string) string {
|
||||
return "test-" + strings.Join(strings.Fields(name), "-")
|
||||
}
|
||||
|
||||
func testContainerCancel(
|
||||
containerExtra func(c *container.Container),
|
||||
waitCheck func(t *testing.T, c *container.Container),
|
||||
) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||
|
||||
c := helperNewContainer(ctx, "block")
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
c.WaitDelay = helperDefaultTimeout
|
||||
if containerExtra != nil {
|
||||
containerExtra(c)
|
||||
}
|
||||
|
||||
ready := make(chan struct{})
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
t.Fatalf("cannot pipe: %v", err)
|
||||
} else {
|
||||
c.ExtraFiles = append(c.ExtraFiles, w)
|
||||
go func() {
|
||||
defer close(ready)
|
||||
if _, err = r.Read(make([]byte, 1)); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if err := c.Start(); err != nil {
|
||||
container.GetOutput().PrintBaseErr(err, "start:")
|
||||
t.Fatalf("cannot start container: %v", err)
|
||||
} else if err = c.Serve(); err != nil {
|
||||
container.GetOutput().PrintBaseErr(err, "serve:")
|
||||
t.Errorf("cannot serve setup params: %v", err)
|
||||
}
|
||||
<-ready
|
||||
cancel()
|
||||
waitCheck(t, c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerString(t *testing.T) {
|
||||
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
|
||||
c.SeccompFlags |= seccomp.AllowMultiarch
|
||||
c.SeccompRules = seccomp.Preset(
|
||||
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
|
||||
c.SeccompFlags)
|
||||
c.SeccompPresets = seccomp.PresetStrict
|
||||
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
|
||||
if got := c.String(); got != want {
|
||||
t.Errorf("String: %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
blockExitCodeInterrupt = 2
|
||||
)
|
||||
|
||||
func init() {
|
||||
helperCommands = append(helperCommands, func(c command.Command) {
|
||||
c.Command("block", command.UsageInternal, func(args []string) error {
|
||||
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
|
||||
return fmt.Errorf("write to sync pipe: %v", err)
|
||||
}
|
||||
{
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
|
||||
}
|
||||
select {}
|
||||
})
|
||||
|
||||
c.Command("container", command.UsageInternal, func(args []string) error {
|
||||
if len(args) != 1 {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
tc := containerTestCases[0]
|
||||
if i, err := strconv.Atoi(args[0]); err != nil {
|
||||
return fmt.Errorf("cannot parse test case index: %v", err)
|
||||
} else {
|
||||
tc = containerTestCases[i]
|
||||
}
|
||||
|
||||
if uid := syscall.Getuid(); uid != tc.uid {
|
||||
return fmt.Errorf("uid: %d, want %d", uid, tc.uid)
|
||||
}
|
||||
if gid := syscall.Getgid(); gid != tc.gid {
|
||||
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
|
||||
}
|
||||
|
||||
wantHost := hostnameFromTestCase(tc.name)
|
||||
if host, err := os.Hostname(); err != nil {
|
||||
return fmt.Errorf("cannot get hostname: %v", err)
|
||||
} else if host != wantHost {
|
||||
return fmt.Errorf("hostname: %q, want %q", host, wantHost)
|
||||
}
|
||||
|
||||
if p, err := os.ReadFile("/etc/hostname"); err != nil {
|
||||
return fmt.Errorf("cannot read /etc/hostname: %v", err)
|
||||
} else if string(p) != wantHost {
|
||||
return fmt.Errorf("/etc/hostname: %q, want %q", string(p), wantHost)
|
||||
}
|
||||
|
||||
if _, err := os.Create(pathReadonly + "/nonexistent"); !errors.Is(err, syscall.EROFS) {
|
||||
return err
|
||||
}
|
||||
|
||||
{
|
||||
var fail bool
|
||||
|
||||
var mnt []*vfs.MountInfoEntry
|
||||
if f, err := os.Open(pathWantMnt); err != nil {
|
||||
return fmt.Errorf("cannot open expected mount points: %v", err)
|
||||
} else if err = gob.NewDecoder(f).Decode(&mnt); err != nil {
|
||||
return fmt.Errorf("cannot parse expected mount points: %v", err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
return fmt.Errorf("cannot close expected mount points: %v", err)
|
||||
}
|
||||
|
||||
if tc.ro && len(mnt) > 0 {
|
||||
// Remount("/", syscall.MS_RDONLY)
|
||||
mnt[0].VfsOptstr = "ro,nosuid,nodev"
|
||||
}
|
||||
|
||||
var d *vfs.MountInfoDecoder
|
||||
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
|
||||
return fmt.Errorf("cannot open mountinfo: %v", err)
|
||||
} else {
|
||||
d = vfs.NewMountInfoDecoder(f)
|
||||
}
|
||||
|
||||
i := 0
|
||||
for cur := range d.Entries() {
|
||||
if i == len(mnt) {
|
||||
return fmt.Errorf("got more than %d entries", len(mnt))
|
||||
}
|
||||
|
||||
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
|
||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
|
||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
|
||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
|
||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
|
||||
|
||||
if !cur.EqualWithIgnore(mnt[i], "\x00") {
|
||||
fail = true
|
||||
log.Printf("[FAIL] %s", cur)
|
||||
} else {
|
||||
log.Printf("[ OK ] %s", cur)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
if err := d.Err(); err != nil {
|
||||
return fmt.Errorf("cannot parse mountinfo: %v", err)
|
||||
}
|
||||
|
||||
if i != len(mnt) {
|
||||
return fmt.Errorf("got %d entries, want %d", i, len(mnt))
|
||||
}
|
||||
|
||||
if fail {
|
||||
return errors.New("one or more mountinfo entries do not match")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
envDoCheck = "HAKUREI_TEST_DO_CHECK"
|
||||
|
||||
helperDefaultTimeout = 5 * time.Second
|
||||
helperInnerPath = "/usr/bin/helper"
|
||||
)
|
||||
|
||||
var (
|
||||
absHelperInnerPath = container.MustAbs(helperInnerPath)
|
||||
)
|
||||
|
||||
var helperCommands []func(c command.Command)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||
|
||||
if os.Getenv(envDoCheck) == "1" {
|
||||
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("helper: ")
|
||||
return nil
|
||||
})
|
||||
for _, f := range helperCommands {
|
||||
f(c)
|
||||
}
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
|
||||
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
|
||||
c.Env = append(c.Env, envDoCheck+"=1")
|
||||
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
|
||||
|
||||
// in case test has cgo enabled
|
||||
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
|
||||
log.Fatalf("ldd: %v", err)
|
||||
} else {
|
||||
*libPaths = ldd.Path(entries)
|
||||
}
|
||||
for _, name := range *libPaths {
|
||||
c.Bind(name, name, 0)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
|
||||
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
|
||||
}
|
||||
245
container/dispatcher.go
Normal file
245
container/dispatcher.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
type osFile interface {
|
||||
Name() string
|
||||
io.Writer
|
||||
fs.File
|
||||
}
|
||||
|
||||
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
|
||||
type syscallDispatcher interface {
|
||||
// new starts a goroutine with a new instance of syscallDispatcher.
|
||||
// A syscallDispatcher must never be used in any goroutine other than the one owning it,
|
||||
// just synchronising access is not enough, as this is for test instrumentation.
|
||||
new(f func(k syscallDispatcher))
|
||||
|
||||
// lockOSThread provides [runtime.LockOSThread].
|
||||
lockOSThread()
|
||||
|
||||
// setPtracer provides [SetPtracer].
|
||||
setPtracer(pid uintptr) error
|
||||
// setDumpable provides [SetDumpable].
|
||||
setDumpable(dumpable uintptr) error
|
||||
// setNoNewPrivs provides [SetNoNewPrivs].
|
||||
setNoNewPrivs() error
|
||||
|
||||
// lastcap provides [LastCap].
|
||||
lastcap() uintptr
|
||||
// capset provides capset.
|
||||
capset(hdrp *capHeader, datap *[2]capData) error
|
||||
// capBoundingSetDrop provides capBoundingSetDrop.
|
||||
capBoundingSetDrop(cap uintptr) error
|
||||
// capAmbientClearAll provides capAmbientClearAll.
|
||||
capAmbientClearAll() error
|
||||
// capAmbientRaise provides capAmbientRaise.
|
||||
capAmbientRaise(cap uintptr) error
|
||||
// isatty provides [Isatty].
|
||||
isatty(fd int) bool
|
||||
// receive provides [Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
|
||||
// bindMount provides procPaths.bindMount.
|
||||
bindMount(source, target string, flags uintptr, eq bool) error
|
||||
// remount provides procPaths.remount.
|
||||
remount(target string, flags uintptr) error
|
||||
// mountTmpfs provides mountTmpfs.
|
||||
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
|
||||
// ensureFile provides ensureFile.
|
||||
ensureFile(name string, perm, pperm os.FileMode) error
|
||||
|
||||
// seccompLoad provides [seccomp.Load].
|
||||
seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error
|
||||
// notify provides [signal.Notify].
|
||||
notify(c chan<- os.Signal, sig ...os.Signal)
|
||||
// start starts [os/exec.Cmd].
|
||||
start(c *exec.Cmd) error
|
||||
// signal signals the underlying process of [os/exec.Cmd].
|
||||
signal(c *exec.Cmd, sig os.Signal) error
|
||||
// evalSymlinks provides [filepath.EvalSymlinks].
|
||||
evalSymlinks(path string) (string, error)
|
||||
|
||||
// exit provides [os.Exit].
|
||||
exit(code int)
|
||||
// getpid provides [os.Getpid].
|
||||
getpid() int
|
||||
// stat provides [os.Stat].
|
||||
stat(name string) (os.FileInfo, error)
|
||||
// mkdir provides [os.Mkdir].
|
||||
mkdir(name string, perm os.FileMode) error
|
||||
// mkdirTemp provides [os.MkdirTemp].
|
||||
mkdirTemp(dir, pattern string) (string, error)
|
||||
// mkdirAll provides [os.MkdirAll].
|
||||
mkdirAll(path string, perm os.FileMode) error
|
||||
// readdir provides [os.ReadDir].
|
||||
readdir(name string) ([]os.DirEntry, error)
|
||||
// openNew provides [os.Open].
|
||||
openNew(name string) (osFile, error)
|
||||
// writeFile provides [os.WriteFile].
|
||||
writeFile(name string, data []byte, perm os.FileMode) error
|
||||
// createTemp provides [os.CreateTemp].
|
||||
createTemp(dir, pattern string) (osFile, error)
|
||||
// remove provides os.Remove.
|
||||
remove(name string) error
|
||||
// newFile provides os.NewFile.
|
||||
newFile(fd uintptr, name string) *os.File
|
||||
// symlink provides os.Symlink.
|
||||
symlink(oldname, newname string) error
|
||||
// readlink provides [os.Readlink].
|
||||
readlink(name string) (string, error)
|
||||
|
||||
// umask provides syscall.Umask.
|
||||
umask(mask int) (oldmask int)
|
||||
// sethostname provides syscall.Sethostname
|
||||
sethostname(p []byte) (err error)
|
||||
// chdir provides syscall.Chdir
|
||||
chdir(path string) (err error)
|
||||
// fchdir provides syscall.Fchdir
|
||||
fchdir(fd int) (err error)
|
||||
// open provides syscall.Open
|
||||
open(path string, mode int, perm uint32) (fd int, err error)
|
||||
// close provides syscall.Close
|
||||
close(fd int) (err error)
|
||||
// pivotRoot provides syscall.PivotRoot
|
||||
pivotRoot(newroot, putold string) (err error)
|
||||
// mount provides syscall.Mount
|
||||
mount(source, target, fstype string, flags uintptr, data string) (err error)
|
||||
// unmount provides syscall.Unmount
|
||||
unmount(target string, flags int) (err error)
|
||||
// wait4 provides syscall.Wait4
|
||||
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
|
||||
|
||||
// printf provides [log.Printf].
|
||||
printf(format string, v ...any)
|
||||
// fatal provides [log.Fatal]
|
||||
fatal(v ...any)
|
||||
// fatalf provides [log.Fatalf]
|
||||
fatalf(format string, v ...any)
|
||||
// verbose provides [Msg.Verbose].
|
||||
verbose(v ...any)
|
||||
// verbosef provides [Msg.Verbosef].
|
||||
verbosef(format string, v ...any)
|
||||
// suspend provides [Msg.Suspend].
|
||||
suspend()
|
||||
// resume provides [Msg.Resume].
|
||||
resume() bool
|
||||
// beforeExit provides [Msg.BeforeExit].
|
||||
beforeExit()
|
||||
// printBaseErr provides [Msg.PrintBaseErr].
|
||||
printBaseErr(err error, fallback string)
|
||||
}
|
||||
|
||||
// direct implements syscallDispatcher on the current kernel.
|
||||
type direct struct{}
|
||||
|
||||
func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
|
||||
|
||||
func (direct) lockOSThread() { runtime.LockOSThread() }
|
||||
|
||||
func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
|
||||
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
|
||||
|
||||
func (direct) lastcap() uintptr { return LastCap() }
|
||||
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
||||
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
|
||||
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
||||
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
||||
func (direct) isatty(fd int) bool { return Isatty(fd) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
return hostProc.bindMount(source, target, flags, eq)
|
||||
}
|
||||
func (direct) remount(target string, flags uintptr) error {
|
||||
return hostProc.remount(target, flags)
|
||||
}
|
||||
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||
return mountTmpfs(k, fsname, target, flags, size, perm)
|
||||
}
|
||||
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||
return ensureFile(name, perm, pperm)
|
||||
}
|
||||
|
||||
func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||
return seccomp.Load(rules, flags)
|
||||
}
|
||||
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
|
||||
func (direct) start(c *exec.Cmd) error { return c.Start() }
|
||||
func (direct) signal(c *exec.Cmd, sig os.Signal) error { return c.Process.Signal(sig) }
|
||||
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
|
||||
|
||||
func (direct) exit(code int) { os.Exit(code) }
|
||||
func (direct) getpid() int { return os.Getpid() }
|
||||
func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) }
|
||||
func (direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) }
|
||||
func (direct) mkdirTemp(dir, pattern string) (string, error) { return os.MkdirTemp(dir, pattern) }
|
||||
func (direct) mkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) }
|
||||
func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
|
||||
func (direct) openNew(name string) (osFile, error) { return os.Open(name) }
|
||||
func (direct) writeFile(name string, data []byte, perm os.FileMode) error {
|
||||
return os.WriteFile(name, data, perm)
|
||||
}
|
||||
func (direct) createTemp(dir, pattern string) (osFile, error) {
|
||||
return os.CreateTemp(dir, pattern)
|
||||
}
|
||||
func (direct) remove(name string) error {
|
||||
return os.Remove(name)
|
||||
}
|
||||
func (direct) newFile(fd uintptr, name string) *os.File {
|
||||
return os.NewFile(fd, name)
|
||||
}
|
||||
func (direct) symlink(oldname, newname string) error {
|
||||
return os.Symlink(oldname, newname)
|
||||
}
|
||||
func (direct) readlink(name string) (string, error) {
|
||||
return os.Readlink(name)
|
||||
}
|
||||
|
||||
func (direct) umask(mask int) (oldmask int) { return syscall.Umask(mask) }
|
||||
func (direct) sethostname(p []byte) (err error) { return syscall.Sethostname(p) }
|
||||
func (direct) chdir(path string) (err error) { return syscall.Chdir(path) }
|
||||
func (direct) fchdir(fd int) (err error) { return syscall.Fchdir(fd) }
|
||||
func (direct) open(path string, mode int, perm uint32) (fd int, err error) {
|
||||
return syscall.Open(path, mode, perm)
|
||||
}
|
||||
func (direct) close(fd int) (err error) {
|
||||
return syscall.Close(fd)
|
||||
}
|
||||
func (direct) pivotRoot(newroot, putold string) (err error) {
|
||||
return syscall.PivotRoot(newroot, putold)
|
||||
}
|
||||
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) {
|
||||
return syscall.Mount(source, target, fstype, flags, data)
|
||||
}
|
||||
func (direct) unmount(target string, flags int) (err error) {
|
||||
return syscall.Unmount(target, flags)
|
||||
}
|
||||
func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
|
||||
return syscall.Wait4(pid, wstatus, options, rusage)
|
||||
}
|
||||
|
||||
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
|
||||
func (direct) fatal(v ...any) { log.Fatal(v...) }
|
||||
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
|
||||
func (direct) verbose(v ...any) { msg.Verbose(v...) }
|
||||
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
|
||||
func (direct) suspend() { msg.Suspend() }
|
||||
func (direct) resume() bool { return msg.Resume() }
|
||||
func (direct) beforeExit() { msg.BeforeExit() }
|
||||
func (direct) printBaseErr(err error, fallback string) { msg.PrintBaseErr(err, fallback) }
|
||||
748
container/dispatcher_test.go
Normal file
748
container/dispatcher_test.go
Normal file
@@ -0,0 +1,748 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
var errUnique = errors.New("unique error injected by the test suite")
|
||||
|
||||
type opValidTestCase struct {
|
||||
name string
|
||||
op Op
|
||||
want bool
|
||||
}
|
||||
|
||||
func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.op.Valid(); got != tc.want {
|
||||
t.Errorf("Valid: %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type opsBuilderTestCase struct {
|
||||
name string
|
||||
ops *Ops
|
||||
want Ops
|
||||
}
|
||||
|
||||
func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
|
||||
t.Run("build", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
|
||||
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type opIsTestCase struct {
|
||||
name string
|
||||
op, v Op
|
||||
want bool
|
||||
}
|
||||
|
||||
func checkOpIs(t *testing.T, testCases []opIsTestCase) {
|
||||
t.Run("is", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.op.Is(tc.v); got != tc.want {
|
||||
t.Errorf("Is: %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type opMetaTestCase struct {
|
||||
name string
|
||||
op Op
|
||||
|
||||
wantPrefix string
|
||||
wantString string
|
||||
}
|
||||
|
||||
func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
|
||||
t.Run("meta", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run("prefix", func(t *testing.T) {
|
||||
if got := tc.op.prefix(); got != tc.wantPrefix {
|
||||
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("string", func(t *testing.T) {
|
||||
if got := tc.op.String(); got != tc.wantString {
|
||||
t.Errorf("String: %s, want %s", got, tc.wantString)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type simpleTestCase struct {
|
||||
name string
|
||||
f func(k syscallDispatcher) error
|
||||
want [][]kexpect
|
||||
wantErr error
|
||||
}
|
||||
|
||||
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer handleExitStub()
|
||||
k := &kstub{t: t, want: tc.want, wg: new(sync.WaitGroup)}
|
||||
if err := tc.f(k); !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
||||
}
|
||||
k.handleIncomplete(func(k *kstub) {
|
||||
t.Errorf("%s: %d calls, want %d (track %d)", fname, k.pos, len(k.want[k.track]), k.track)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type opBehaviourTestCase struct {
|
||||
name string
|
||||
params *Params
|
||||
op Op
|
||||
|
||||
early []kexpect
|
||||
wantErrEarly error
|
||||
|
||||
apply []kexpect
|
||||
wantErrApply error
|
||||
}
|
||||
|
||||
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
||||
t.Run("behaviour", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer handleExitStub()
|
||||
state := &setupState{Params: tc.params}
|
||||
k := &kstub{t: t, want: [][]kexpect{slices.Concat(tc.early, []kexpect{{name: "\x00"}}, tc.apply)}, wg: new(sync.WaitGroup)}
|
||||
errEarly := tc.op.early(state, k)
|
||||
k.expect("\x00")
|
||||
if !errors.Is(errEarly, tc.wantErrEarly) {
|
||||
t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly)
|
||||
}
|
||||
if errEarly != nil {
|
||||
goto out
|
||||
}
|
||||
|
||||
if err := tc.op.apply(state, k); !errors.Is(err, tc.wantErrApply) {
|
||||
t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply)
|
||||
}
|
||||
|
||||
out:
|
||||
k.handleIncomplete(func(k *kstub) {
|
||||
count := k.pos - 1 // separator
|
||||
if count < len(tc.early) {
|
||||
t.Errorf("early: %d calls, want %d", count, len(tc.early))
|
||||
} else {
|
||||
t.Errorf("apply: %d calls, want %d", count-len(tc.early), len(tc.apply))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func sliceAddr[S any](s []S) *[]S { return &s }
|
||||
|
||||
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
|
||||
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
|
||||
// check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results
|
||||
f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name)
|
||||
return f
|
||||
}
|
||||
|
||||
type checkedOsFile struct {
|
||||
t *testing.T
|
||||
name string
|
||||
want string
|
||||
closeErr error
|
||||
cleanup runtime.Cleanup
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (f *checkedOsFile) Name() string { return f.name }
|
||||
func (f *checkedOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
|
||||
func (f *checkedOsFile) Close() error {
|
||||
defer f.cleanup.Stop()
|
||||
if f.String() != f.want {
|
||||
f.t.Errorf("checkedOsFile:\n%s\nwant\n%s", f.String(), f.want)
|
||||
return syscall.ENOTRECOVERABLE
|
||||
}
|
||||
return f.closeErr
|
||||
}
|
||||
|
||||
func newConstFile(s string) osFile { return &readerOsFile{Reader: strings.NewReader(s)} }
|
||||
|
||||
type readerOsFile struct {
|
||||
closed bool
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (*readerOsFile) Name() string { panic("unreachable") }
|
||||
func (*readerOsFile) Write([]byte) (int, error) { panic("unreachable") }
|
||||
func (*readerOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
|
||||
func (r *readerOsFile) Close() error {
|
||||
if r.closed {
|
||||
return os.ErrClosed
|
||||
}
|
||||
r.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type writeErrOsFile struct{ err error }
|
||||
|
||||
func (writeErrOsFile) Name() string { panic("unreachable") }
|
||||
func (f writeErrOsFile) Write([]byte) (int, error) { return 0, f.err }
|
||||
func (writeErrOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
|
||||
func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") }
|
||||
func (writeErrOsFile) Close() error { panic("unreachable") }
|
||||
|
||||
type expectArgs = [5]any
|
||||
|
||||
type isDirFi bool
|
||||
|
||||
func (isDirFi) Name() string { panic("unreachable") }
|
||||
func (isDirFi) Size() int64 { panic("unreachable") }
|
||||
func (isDirFi) Mode() fs.FileMode { panic("unreachable") }
|
||||
func (isDirFi) ModTime() time.Time { panic("unreachable") }
|
||||
func (fi isDirFi) IsDir() bool { return bool(fi) }
|
||||
func (isDirFi) Sys() any { panic("unreachable") }
|
||||
|
||||
func stubDir(names ...string) []os.DirEntry {
|
||||
d := make([]os.DirEntry, len(names))
|
||||
for i, name := range names {
|
||||
d[i] = nameDentry(name)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
type nameDentry string
|
||||
|
||||
func (e nameDentry) Name() string { return string(e) }
|
||||
func (nameDentry) IsDir() bool { panic("unreachable") }
|
||||
func (nameDentry) Type() fs.FileMode { panic("unreachable") }
|
||||
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
|
||||
|
||||
type kexpect struct {
|
||||
name string
|
||||
args expectArgs
|
||||
ret any
|
||||
err error
|
||||
}
|
||||
|
||||
func (k *kexpect) error(ok ...bool) error {
|
||||
if !slices.Contains(ok, false) {
|
||||
return k.err
|
||||
}
|
||||
return syscall.ENOTRECOVERABLE
|
||||
}
|
||||
|
||||
func handleExitStub() {
|
||||
r := recover()
|
||||
if r == 0xdeadbeef {
|
||||
return
|
||||
}
|
||||
if r != nil {
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
|
||||
type kstub struct {
|
||||
t *testing.T
|
||||
|
||||
want [][]kexpect
|
||||
// pos is the current position in want[track].
|
||||
pos int
|
||||
// track is the current active want.
|
||||
track int
|
||||
// sub stores addresses of kstub created by new.
|
||||
sub []*kstub
|
||||
// wg waits for all descendants to complete.
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// handleIncomplete calls f on an incomplete k and all its descendants.
|
||||
func (k *kstub) handleIncomplete(f func(k *kstub)) {
|
||||
k.wg.Wait()
|
||||
|
||||
if k.want != nil && len(k.want[k.track]) != k.pos {
|
||||
f(k)
|
||||
}
|
||||
for _, sk := range k.sub {
|
||||
sk.handleIncomplete(f)
|
||||
}
|
||||
}
|
||||
|
||||
// expect checks name and returns the current kexpect and advances pos.
|
||||
func (k *kstub) expect(name string) (expect *kexpect) {
|
||||
if len(k.want[k.track]) == k.pos {
|
||||
k.t.Fatal("expect: want too short")
|
||||
}
|
||||
expect = &k.want[k.track][k.pos]
|
||||
if name != expect.name {
|
||||
if expect.name == "\x00" {
|
||||
k.t.Fatalf("expect: func = %s, separator overrun", name)
|
||||
}
|
||||
if name == "\x00" {
|
||||
k.t.Fatalf("expect: separator, want %s", expect.name)
|
||||
}
|
||||
k.t.Fatalf("expect: func = %s, want %s", name, expect.name)
|
||||
}
|
||||
k.pos++
|
||||
return
|
||||
}
|
||||
|
||||
// checkArg checks an argument comparable with the == operator. Avoid using this with pointers.
|
||||
func checkArg[T comparable](k *kstub, arg string, got T, n int) bool {
|
||||
if k.pos == 0 {
|
||||
panic("invalid call to checkArg")
|
||||
}
|
||||
expect := k.want[k.track][k.pos-1]
|
||||
want, ok := expect.args[n].(T)
|
||||
if !ok || got != want {
|
||||
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkArgReflect checks an argument of any type.
|
||||
func checkArgReflect(k *kstub, arg string, got any, n int) bool {
|
||||
if k.pos == 0 {
|
||||
panic("invalid call to checkArgReflect")
|
||||
}
|
||||
expect := k.want[k.track][k.pos-1]
|
||||
want := expect.args[n]
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (k *kstub) new(f func(k syscallDispatcher)) {
|
||||
k.expect("new")
|
||||
if len(k.want) <= k.track+1 {
|
||||
k.t.Fatalf("new: track overrun")
|
||||
}
|
||||
sk := &kstub{t: k.t, want: k.want, track: len(k.sub) + 1, wg: k.wg}
|
||||
k.sub = append(k.sub, sk)
|
||||
k.wg.Add(1)
|
||||
go func() {
|
||||
defer k.wg.Done()
|
||||
defer handleExitStub()
|
||||
f(sk)
|
||||
}()
|
||||
}
|
||||
|
||||
func (k *kstub) lockOSThread() { k.expect("lockOSThread") }
|
||||
|
||||
func (k *kstub) setPtracer(pid uintptr) error {
|
||||
return k.expect("setPtracer").error(
|
||||
checkArg(k, "pid", pid, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) setDumpable(dumpable uintptr) error {
|
||||
return k.expect("setDumpable").error(
|
||||
checkArg(k, "dumpable", dumpable, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) setNoNewPrivs() error { return k.expect("setNoNewPrivs").err }
|
||||
func (k *kstub) lastcap() uintptr { return k.expect("lastcap").ret.(uintptr) }
|
||||
|
||||
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
|
||||
return k.expect("capset").error(
|
||||
checkArgReflect(k, "hdrp", hdrp, 0),
|
||||
checkArgReflect(k, "datap", datap, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) capBoundingSetDrop(cap uintptr) error {
|
||||
return k.expect("capBoundingSetDrop").error(
|
||||
checkArg(k, "cap", cap, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) capAmbientClearAll() error { return k.expect("capAmbientClearAll").err }
|
||||
|
||||
func (k *kstub) capAmbientRaise(cap uintptr) error {
|
||||
return k.expect("capAmbientRaise").error(
|
||||
checkArg(k, "cap", cap, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) isatty(fd int) bool {
|
||||
expect := k.expect("isatty")
|
||||
if !checkArg(k, "fd", fd, 0) {
|
||||
k.t.FailNow()
|
||||
}
|
||||
return expect.ret.(bool)
|
||||
}
|
||||
|
||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||
expect := k.expect("receive")
|
||||
|
||||
var closed bool
|
||||
closeFunc = func() error {
|
||||
if closed {
|
||||
k.t.Error("closeFunc called more than once")
|
||||
return os.ErrClosed
|
||||
}
|
||||
closed = true
|
||||
|
||||
if expect.ret != nil {
|
||||
// use return stored in kexpect for closeFunc instead
|
||||
return expect.ret.(error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err = expect.error(
|
||||
checkArg(k, "key", key, 0),
|
||||
checkArgReflect(k, "e", e, 1),
|
||||
checkArgReflect(k, "fdp", fdp, 2))
|
||||
|
||||
// 3 is unused so stores params
|
||||
if expect.args[3] != nil {
|
||||
if v, ok := expect.args[3].(*initParams); ok && v != nil {
|
||||
if p, ok0 := e.(*initParams); ok0 && p != nil {
|
||||
*p = *v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4 is unused so stores fd
|
||||
if expect.args[4] != nil {
|
||||
if v, ok := expect.args[4].(uintptr); ok && v >= 3 {
|
||||
if fdp != nil {
|
||||
*fdp = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
return k.expect("bindMount").error(
|
||||
checkArg(k, "source", source, 0),
|
||||
checkArg(k, "target", target, 1),
|
||||
checkArg(k, "flags", flags, 2),
|
||||
checkArg(k, "eq", eq, 3))
|
||||
}
|
||||
|
||||
func (k *kstub) remount(target string, flags uintptr) error {
|
||||
return k.expect("remount").error(
|
||||
checkArg(k, "target", target, 0),
|
||||
checkArg(k, "flags", flags, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||
return k.expect("mountTmpfs").error(
|
||||
checkArg(k, "fsname", fsname, 0),
|
||||
checkArg(k, "target", target, 1),
|
||||
checkArg(k, "flags", flags, 2),
|
||||
checkArg(k, "size", size, 3),
|
||||
checkArg(k, "perm", perm, 4))
|
||||
}
|
||||
|
||||
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||
|
||||
return k.expect("ensureFile").error(
|
||||
checkArg(k, "name", name, 0),
|
||||
checkArg(k, "perm", perm, 1),
|
||||
checkArg(k, "pperm", pperm, 2))
|
||||
}
|
||||
|
||||
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||
return k.expect("seccompLoad").error(
|
||||
checkArgReflect(k, "rules", rules, 0),
|
||||
checkArg(k, "flags", flags, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
||||
expect := k.expect("notify")
|
||||
if c == nil || expect.error(
|
||||
checkArgReflect(k, "sig", sig, 1)) != nil {
|
||||
k.t.FailNow()
|
||||
}
|
||||
|
||||
// export channel for external instrumentation
|
||||
if chanf, ok := expect.args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
||||
chanf(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) start(c *exec.Cmd) error {
|
||||
expect := k.expect("start")
|
||||
err := expect.error(
|
||||
checkArg(k, "c.Path", c.Path, 0),
|
||||
checkArgReflect(k, "c.Args", c.Args, 1),
|
||||
checkArgReflect(k, "c.Env", c.Env, 2),
|
||||
checkArg(k, "c.Dir", c.Dir, 3))
|
||||
|
||||
if process, ok := expect.ret.(*os.Process); ok && process != nil {
|
||||
c.Process = process
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error {
|
||||
return k.expect("signal").error(
|
||||
checkArg(k, "c.Path", c.Path, 0),
|
||||
checkArgReflect(k, "c.Args", c.Args, 1),
|
||||
checkArgReflect(k, "c.Env", c.Env, 2),
|
||||
checkArg(k, "c.Dir", c.Dir, 3),
|
||||
checkArg(k, "sig", sig, 4))
|
||||
}
|
||||
|
||||
func (k *kstub) evalSymlinks(path string) (string, error) {
|
||||
expect := k.expect("evalSymlinks")
|
||||
return expect.ret.(string), expect.error(
|
||||
checkArg(k, "path", path, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) exit(code int) {
|
||||
k.expect("exit")
|
||||
if !checkArg(k, "code", code, 0) {
|
||||
k.t.FailNow()
|
||||
}
|
||||
panic(0xdeadbeef)
|
||||
}
|
||||
|
||||
func (k *kstub) getpid() int { return k.expect("getpid").ret.(int) }
|
||||
|
||||
func (k *kstub) stat(name string) (os.FileInfo, error) {
|
||||
expect := k.expect("stat")
|
||||
return expect.ret.(os.FileInfo), expect.error(
|
||||
checkArg(k, "name", name, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) mkdir(name string, perm os.FileMode) error {
|
||||
return k.expect("mkdir").error(
|
||||
checkArg(k, "name", name, 0),
|
||||
checkArg(k, "perm", perm, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) mkdirTemp(dir, pattern string) (string, error) {
|
||||
expect := k.expect("mkdirTemp")
|
||||
return expect.ret.(string), expect.error(
|
||||
checkArg(k, "dir", dir, 0),
|
||||
checkArg(k, "pattern", pattern, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) mkdirAll(path string, perm os.FileMode) error {
|
||||
return k.expect("mkdirAll").error(
|
||||
checkArg(k, "path", path, 0),
|
||||
checkArg(k, "perm", perm, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
|
||||
expect := k.expect("readdir")
|
||||
return expect.ret.([]os.DirEntry), expect.error(
|
||||
checkArg(k, "name", name, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) openNew(name string) (osFile, error) {
|
||||
expect := k.expect("openNew")
|
||||
return expect.ret.(osFile), expect.error(
|
||||
checkArg(k, "name", name, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error {
|
||||
return k.expect("writeFile").error(
|
||||
checkArg(k, "name", name, 0),
|
||||
checkArgReflect(k, "data", data, 1),
|
||||
checkArg(k, "perm", perm, 2))
|
||||
}
|
||||
|
||||
func (k *kstub) createTemp(dir, pattern string) (osFile, error) {
|
||||
expect := k.expect("createTemp")
|
||||
return expect.ret.(osFile), expect.error(
|
||||
checkArg(k, "dir", dir, 0),
|
||||
checkArg(k, "pattern", pattern, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) remove(name string) error {
|
||||
return k.expect("remove").error(
|
||||
checkArg(k, "name", name, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) newFile(fd uintptr, name string) *os.File {
|
||||
expect := k.expect("newFile")
|
||||
if expect.error(
|
||||
checkArg(k, "fd", fd, 0),
|
||||
checkArg(k, "name", name, 1)) != nil {
|
||||
k.t.FailNow()
|
||||
}
|
||||
return expect.ret.(*os.File)
|
||||
}
|
||||
|
||||
func (k *kstub) symlink(oldname, newname string) error {
|
||||
return k.expect("symlink").error(
|
||||
checkArg(k, "oldname", oldname, 0),
|
||||
checkArg(k, "newname", newname, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) readlink(name string) (string, error) {
|
||||
expect := k.expect("readlink")
|
||||
return expect.ret.(string), expect.error(
|
||||
checkArg(k, "name", name, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) umask(mask int) (oldmask int) {
|
||||
expect := k.expect("umask")
|
||||
if !checkArg(k, "mask", mask, 0) {
|
||||
k.t.FailNow()
|
||||
}
|
||||
return expect.ret.(int)
|
||||
}
|
||||
|
||||
func (k *kstub) sethostname(p []byte) (err error) {
|
||||
return k.expect("sethostname").error(
|
||||
checkArgReflect(k, "p", p, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) chdir(path string) (err error) {
|
||||
return k.expect("chdir").error(
|
||||
checkArg(k, "path", path, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) fchdir(fd int) (err error) {
|
||||
return k.expect("fchdir").error(
|
||||
checkArg(k, "fd", fd, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) {
|
||||
expect := k.expect("open")
|
||||
return expect.ret.(int), expect.error(
|
||||
checkArg(k, "path", path, 0),
|
||||
checkArg(k, "mode", mode, 1),
|
||||
checkArg(k, "perm", perm, 2))
|
||||
}
|
||||
|
||||
func (k *kstub) close(fd int) (err error) {
|
||||
return k.expect("close").error(
|
||||
checkArg(k, "fd", fd, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) pivotRoot(newroot, putold string) (err error) {
|
||||
return k.expect("pivotRoot").error(
|
||||
checkArg(k, "newroot", newroot, 0),
|
||||
checkArg(k, "putold", putold, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) {
|
||||
return k.expect("mount").error(
|
||||
checkArg(k, "source", source, 0),
|
||||
checkArg(k, "target", target, 1),
|
||||
checkArg(k, "fstype", fstype, 2),
|
||||
checkArg(k, "flags", flags, 3),
|
||||
checkArg(k, "data", data, 4))
|
||||
}
|
||||
|
||||
func (k *kstub) unmount(target string, flags int) (err error) {
|
||||
return k.expect("unmount").error(
|
||||
checkArg(k, "target", target, 0),
|
||||
checkArg(k, "flags", flags, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
|
||||
expect := k.expect("wait4")
|
||||
// special case to prevent leaking the wait4 goroutine when testing initEntrypoint
|
||||
if v, ok := expect.args[4].(int); ok && v == 0xdeadbeef {
|
||||
k.t.Log("terminating current goroutine as requested by kexpect")
|
||||
panic(0xdeadbeef)
|
||||
}
|
||||
|
||||
wpid = expect.ret.(int)
|
||||
err = expect.error(
|
||||
checkArg(k, "pid", pid, 0),
|
||||
checkArg(k, "options", options, 2))
|
||||
|
||||
if wstatusV, ok := expect.args[1].(syscall.WaitStatus); wstatus != nil && ok {
|
||||
*wstatus = wstatusV
|
||||
}
|
||||
if rusageV, ok := expect.args[3].(syscall.Rusage); rusage != nil && ok {
|
||||
*rusage = rusageV
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (k *kstub) printf(format string, v ...any) {
|
||||
if k.expect("printf").error(
|
||||
checkArg(k, "format", format, 0),
|
||||
checkArgReflect(k, "v", v, 1)) != nil {
|
||||
k.t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) fatal(v ...any) {
|
||||
if k.expect("fatal").error(
|
||||
checkArgReflect(k, "v", v, 0)) != nil {
|
||||
k.t.FailNow()
|
||||
}
|
||||
panic(0xdeadbeef)
|
||||
}
|
||||
|
||||
func (k *kstub) fatalf(format string, v ...any) {
|
||||
if k.expect("fatalf").error(
|
||||
checkArg(k, "format", format, 0),
|
||||
checkArgReflect(k, "v", v, 1)) != nil {
|
||||
k.t.FailNow()
|
||||
}
|
||||
panic(0xdeadbeef)
|
||||
}
|
||||
|
||||
func (k *kstub) verbose(v ...any) {
|
||||
if k.expect("verbose").error(
|
||||
checkArgReflect(k, "v", v, 0)) != nil {
|
||||
k.t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) verbosef(format string, v ...any) {
|
||||
if k.expect("verbosef").error(
|
||||
checkArg(k, "format", format, 0),
|
||||
checkArgReflect(k, "v", v, 1)) != nil {
|
||||
k.t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) suspend() { k.expect("suspend") }
|
||||
func (k *kstub) resume() bool { return k.expect("resume").ret.(bool) }
|
||||
func (k *kstub) beforeExit() { k.expect("beforeExit") }
|
||||
|
||||
func (k *kstub) printBaseErr(err error, fallback string) {
|
||||
if k.expect("printBaseErr").error(
|
||||
checkArgReflect(k, "err", err, 0),
|
||||
checkArg(k, "fallback", fallback, 1)) != nil {
|
||||
k.t.FailNow()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package proc
|
||||
package container
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -14,7 +13,8 @@ var (
|
||||
|
||||
func copyExecutable() {
|
||||
if name, err := os.Executable(); err != nil {
|
||||
fmsg.Fatalf("cannot read executable path: %v", err)
|
||||
msg.BeforeExit()
|
||||
log.Fatalf("cannot read executable path: %v", err)
|
||||
} else {
|
||||
executable = name
|
||||
}
|
||||
17
container/executable_test.go
Normal file
17
container/executable_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
func TestExecutable(t *testing.T) {
|
||||
for i := 0; i < 16; i++ {
|
||||
if got := container.MustExecutable(); got != os.Args[0] {
|
||||
t.Errorf("MustExecutable: %q, want %q",
|
||||
got, os.Args[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
435
container/init.go
Normal file
435
container/init.go
Normal file
@@ -0,0 +1,435 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
const (
|
||||
/* intermediate tmpfs mount point
|
||||
|
||||
this path might seem like a weird choice, however there are many good reasons to use it:
|
||||
- the contents of this path is never exposed to the container:
|
||||
the tmpfs root established here effectively becomes anonymous after pivot_root
|
||||
- it is safe to assume this path exists and is a directory:
|
||||
this program will not work correctly without a proper /proc and neither will most others
|
||||
- this path belongs to the container init:
|
||||
the container init is not any more privileged or trusted than the rest of the container
|
||||
- this path is only accessible by init and root:
|
||||
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
|
||||
|
||||
it should be noted that none of this should become relevant at any point since the resulting
|
||||
intermediate root tmpfs should be effectively anonymous */
|
||||
intermediateHostPath = FHSProc + "self/fd"
|
||||
|
||||
// setup params file descriptor
|
||||
setupEnv = "HAKUREI_SETUP"
|
||||
)
|
||||
|
||||
type (
|
||||
// Ops is a collection of [Op].
|
||||
Ops []Op
|
||||
|
||||
// Op is a generic setup step ran inside the container init.
|
||||
// Implementations of this interface are sent as a stream of gobs.
|
||||
Op interface {
|
||||
// early is called in host root.
|
||||
early(state *setupState, k syscallDispatcher) error
|
||||
// apply is called in intermediate root.
|
||||
apply(state *setupState, k syscallDispatcher) error
|
||||
|
||||
prefix() string
|
||||
Is(op Op) bool
|
||||
Valid() bool
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
// setupState persists context between Ops.
|
||||
setupState struct {
|
||||
nonrepeatable uintptr
|
||||
*Params
|
||||
}
|
||||
)
|
||||
|
||||
// Grow grows the slice Ops points to using [slices.Grow].
|
||||
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
||||
|
||||
const (
|
||||
nrAutoEtc = 1 << iota
|
||||
nrAutoRoot
|
||||
)
|
||||
|
||||
// initParams are params passed from parent.
|
||||
type initParams struct {
|
||||
Params
|
||||
|
||||
HostUid, HostGid int
|
||||
// extra files count
|
||||
Count int
|
||||
// verbosity pass through
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) {
|
||||
initEntrypoint(direct{}, prepareLogger, setVerbose)
|
||||
}
|
||||
|
||||
func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
|
||||
k.lockOSThread()
|
||||
prepareLogger("init")
|
||||
|
||||
if k.getpid() != 1 {
|
||||
k.fatal("this process must run as pid 1")
|
||||
}
|
||||
|
||||
if err := k.setPtracer(0); err != nil {
|
||||
k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
|
||||
// not fatal: this program has no additional privileges at initial program start
|
||||
}
|
||||
|
||||
var (
|
||||
params initParams
|
||||
closeSetup func() error
|
||||
setupFd uintptr
|
||||
offsetSetup int
|
||||
)
|
||||
if f, err := k.receive(setupEnv, ¶ms, &setupFd); err != nil {
|
||||
if errors.Is(err, EBADF) {
|
||||
k.fatal("invalid setup descriptor")
|
||||
}
|
||||
if errors.Is(err, ErrNotSet) {
|
||||
k.fatal("HAKUREI_SETUP not set")
|
||||
}
|
||||
|
||||
k.fatalf("cannot decode init setup payload: %v", err)
|
||||
} else {
|
||||
if params.Ops == nil {
|
||||
k.fatal("invalid setup parameters")
|
||||
}
|
||||
if params.ParentPerm == 0 {
|
||||
params.ParentPerm = 0755
|
||||
}
|
||||
|
||||
setVerbose(params.Verbose)
|
||||
k.verbose("received setup parameters")
|
||||
closeSetup = f
|
||||
offsetSetup = int(setupFd + 1)
|
||||
}
|
||||
|
||||
// write uid/gid map here so parent does not need to set dumpable
|
||||
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
|
||||
k.fatalf("cannot set SUID_DUMP_USER: %v", err)
|
||||
}
|
||||
if err := k.writeFile(FHSProc+"self/uid_map",
|
||||
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
|
||||
0); err != nil {
|
||||
k.fatalf("%v", err)
|
||||
}
|
||||
if err := k.writeFile(FHSProc+"self/setgroups",
|
||||
[]byte("deny\n"),
|
||||
0); err != nil && !os.IsNotExist(err) {
|
||||
k.fatalf("%v", err)
|
||||
}
|
||||
if err := k.writeFile(FHSProc+"self/gid_map",
|
||||
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
|
||||
0); err != nil {
|
||||
k.fatalf("%v", err)
|
||||
}
|
||||
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
|
||||
k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err)
|
||||
}
|
||||
|
||||
oldmask := k.umask(0)
|
||||
if params.Hostname != "" {
|
||||
if err := k.sethostname([]byte(params.Hostname)); err != nil {
|
||||
k.fatalf("cannot set hostname: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// cache sysctl before pivot_root
|
||||
lastcap := k.lastcap()
|
||||
|
||||
if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
|
||||
k.fatalf("cannot make / rslave: %v", err)
|
||||
}
|
||||
|
||||
state := &setupState{Params: ¶ms.Params}
|
||||
|
||||
/* early is called right before pivot_root into intermediate root;
|
||||
this step is mostly for gathering information that would otherwise be difficult to obtain
|
||||
via library functions after pivot_root, and implementations are expected to avoid changing
|
||||
the state of the mount namespace */
|
||||
for i, op := range *params.Ops {
|
||||
if op == nil || !op.Valid() {
|
||||
k.fatalf("invalid op at index %d", i)
|
||||
}
|
||||
|
||||
if err := op.early(state, k); err != nil {
|
||||
k.printBaseErr(err,
|
||||
fmt.Sprintf("cannot prepare op at index %d:", i))
|
||||
k.beforeExit()
|
||||
k.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
|
||||
k.fatalf("cannot mount intermediate root: %v", err)
|
||||
}
|
||||
if err := k.chdir(intermediateHostPath); err != nil {
|
||||
k.fatalf("cannot enter intermediate host path: %v", err)
|
||||
}
|
||||
|
||||
if err := k.mkdir(sysrootDir, 0755); err != nil {
|
||||
k.fatalf("%v", err)
|
||||
}
|
||||
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
|
||||
k.fatalf("cannot bind sysroot: %v", err)
|
||||
}
|
||||
|
||||
if err := k.mkdir(hostDir, 0755); err != nil {
|
||||
k.fatalf("%v", err)
|
||||
}
|
||||
// pivot_root uncovers intermediateHostPath in hostDir
|
||||
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
|
||||
k.fatalf("cannot pivot into intermediate root: %v", err)
|
||||
}
|
||||
if err := k.chdir(FHSRoot); err != nil {
|
||||
k.fatalf("cannot enter intermediate root: %v", err)
|
||||
}
|
||||
|
||||
/* apply is called right after pivot_root and entering the new root;
|
||||
this step sets up the container filesystem, and implementations are expected to keep the host root
|
||||
and sysroot mount points intact but otherwise can do whatever they need to;
|
||||
chdir is allowed but discouraged */
|
||||
for i, op := range *params.Ops {
|
||||
// ops already checked during early setup
|
||||
k.verbosef("%s %s", op.prefix(), op)
|
||||
if err := op.apply(state, k); err != nil {
|
||||
k.printBaseErr(err,
|
||||
fmt.Sprintf("cannot apply op at index %d:", i))
|
||||
k.beforeExit()
|
||||
k.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// setup requiring host root complete at this point
|
||||
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
|
||||
k.fatalf("cannot make host root rprivate: %v", err)
|
||||
}
|
||||
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
|
||||
k.fatalf("cannot unmount host root: %v", err)
|
||||
}
|
||||
|
||||
{
|
||||
var fd int
|
||||
if err := IgnoringEINTR(func() (err error) {
|
||||
fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
|
||||
return
|
||||
}); err != nil {
|
||||
k.fatalf("cannot open intermediate root: %v", err)
|
||||
}
|
||||
if err := k.chdir(sysrootPath); err != nil {
|
||||
k.fatalf("cannot enter sysroot: %v", err)
|
||||
}
|
||||
|
||||
if err := k.pivotRoot(".", "."); err != nil {
|
||||
k.fatalf("cannot pivot into sysroot: %v", err)
|
||||
}
|
||||
if err := k.fchdir(fd); err != nil {
|
||||
k.fatalf("cannot re-enter intermediate root: %v", err)
|
||||
}
|
||||
if err := k.unmount(".", MNT_DETACH); err != nil {
|
||||
k.fatalf("cannot unmount intermediate root: %v", err)
|
||||
}
|
||||
if err := k.chdir(FHSRoot); err != nil {
|
||||
k.fatalf("cannot enter root: %v", err)
|
||||
}
|
||||
|
||||
if err := k.close(fd); err != nil {
|
||||
k.fatalf("cannot close intermediate root: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := k.capAmbientClearAll(); err != nil {
|
||||
k.fatalf("cannot clear the ambient capability set: %v", err)
|
||||
}
|
||||
for i := uintptr(0); i <= lastcap; i++ {
|
||||
if params.Privileged && i == CAP_SYS_ADMIN {
|
||||
continue
|
||||
}
|
||||
if err := k.capBoundingSetDrop(i); err != nil {
|
||||
k.fatalf("cannot drop capability from bounding set: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var keep [2]uint32
|
||||
if params.Privileged {
|
||||
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
||||
|
||||
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
|
||||
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err)
|
||||
}
|
||||
}
|
||||
if err := k.capset(
|
||||
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
|
||||
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
|
||||
); err != nil {
|
||||
k.fatalf("cannot capset: %v", err)
|
||||
}
|
||||
|
||||
if !params.SeccompDisable {
|
||||
rules := params.SeccompRules
|
||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||
k.verbosef("resolving presets %#x", params.SeccompPresets)
|
||||
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
|
||||
}
|
||||
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
|
||||
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
|
||||
k.fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
k.verbosef("%d filter rules loaded", len(rules))
|
||||
} else {
|
||||
k.verbose("syscall filter not configured")
|
||||
}
|
||||
|
||||
extraFiles := make([]*os.File, params.Count)
|
||||
for i := range extraFiles {
|
||||
// setup fd is placed before all extra files
|
||||
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
||||
}
|
||||
k.umask(oldmask)
|
||||
|
||||
cmd := exec.Command(params.Path.String())
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.Args = params.Args
|
||||
cmd.Env = params.Env
|
||||
cmd.ExtraFiles = extraFiles
|
||||
cmd.Dir = params.Dir.String()
|
||||
|
||||
k.verbosef("starting initial program %s", params.Path)
|
||||
if err := k.start(cmd); err != nil {
|
||||
k.fatalf("%v", err)
|
||||
}
|
||||
k.suspend()
|
||||
|
||||
if err := closeSetup(); err != nil {
|
||||
k.printf("cannot close setup pipe: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
type winfo struct {
|
||||
wpid int
|
||||
wstatus WaitStatus
|
||||
}
|
||||
info := make(chan winfo, 1)
|
||||
done := make(chan struct{})
|
||||
|
||||
k.new(func(k syscallDispatcher) {
|
||||
var (
|
||||
err error
|
||||
wpid = -2
|
||||
wstatus WaitStatus
|
||||
)
|
||||
|
||||
// keep going until no child process is left
|
||||
for wpid != -1 {
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if wpid != -2 {
|
||||
info <- winfo{wpid, wstatus}
|
||||
}
|
||||
|
||||
err = EINTR
|
||||
for errors.Is(err, EINTR) {
|
||||
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
||||
}
|
||||
}
|
||||
if !errors.Is(err, ECHILD) {
|
||||
k.printf("unexpected wait4 response: %v", err)
|
||||
}
|
||||
|
||||
close(done)
|
||||
})
|
||||
|
||||
// handle signals to dump withheld messages
|
||||
sig := make(chan os.Signal, 2)
|
||||
k.notify(sig, os.Interrupt, CancelSignal)
|
||||
|
||||
// closed after residualProcessTimeout has elapsed after initial process death
|
||||
timeout := make(chan struct{})
|
||||
|
||||
r := 2
|
||||
for {
|
||||
select {
|
||||
case s := <-sig:
|
||||
if k.resume() {
|
||||
k.verbosef("%s after process start", s.String())
|
||||
} else {
|
||||
k.verbosef("got %s", s.String())
|
||||
}
|
||||
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
|
||||
k.verbose("forwarding context cancellation")
|
||||
if err := k.signal(cmd, os.Interrupt); err != nil {
|
||||
k.printf("cannot forward cancellation: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
k.beforeExit()
|
||||
k.exit(0)
|
||||
|
||||
case w := <-info:
|
||||
if w.wpid == cmd.Process.Pid {
|
||||
// initial process exited, output is most likely available again
|
||||
k.resume()
|
||||
|
||||
switch {
|
||||
case w.wstatus.Exited():
|
||||
r = w.wstatus.ExitStatus()
|
||||
k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
|
||||
|
||||
case w.wstatus.Signaled():
|
||||
r = 128 + int(w.wstatus.Signal())
|
||||
k.verbosef("initial process exited with signal %s", w.wstatus.Signal())
|
||||
|
||||
default:
|
||||
r = 255
|
||||
k.verbosef("initial process exited with status %#x", w.wstatus)
|
||||
}
|
||||
|
||||
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
||||
}
|
||||
|
||||
case <-done:
|
||||
k.beforeExit()
|
||||
k.exit(r)
|
||||
|
||||
case <-timeout:
|
||||
k.printf("timeout exceeded waiting for lingering processes")
|
||||
k.beforeExit()
|
||||
k.exit(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initName = "init"
|
||||
|
||||
// TryArgv0 calls [Init] if the last element of argv0 is "init".
|
||||
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
|
||||
msg = v
|
||||
Init(prepare, setVerbose)
|
||||
msg.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
2539
container/init_test.go
Normal file
2539
container/init_test.go
Normal file
File diff suppressed because it is too large
Load Diff
113
container/initbind.go
Normal file
113
container/initbind.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(BindMountOp)) }
|
||||
|
||||
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
|
||||
func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
|
||||
*f = append(*f, &BindMountOp{nil, source, target, flags})
|
||||
return f
|
||||
}
|
||||
|
||||
// BindMountOp bind mounts host path Source on container path Target.
|
||||
// Note that Flags uses bits declared in this package and should not be set with constants in [syscall].
|
||||
type BindMountOp struct {
|
||||
sourceFinal, Source, Target *Absolute
|
||||
|
||||
Flags int
|
||||
}
|
||||
|
||||
const (
|
||||
// BindOptional skips nonexistent host paths.
|
||||
BindOptional = 1 << iota
|
||||
// BindWritable mounts filesystem read-write.
|
||||
BindWritable
|
||||
// BindDevice allows access to devices (special files) on this filesystem.
|
||||
BindDevice
|
||||
// BindEnsure attempts to create the host path if it does not exist.
|
||||
BindEnsure
|
||||
)
|
||||
|
||||
func (b *BindMountOp) Valid() bool {
|
||||
return b != nil &&
|
||||
b.Source != nil && b.Target != nil &&
|
||||
b.Flags&(BindOptional|BindEnsure) != (BindOptional|BindEnsure)
|
||||
}
|
||||
|
||||
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
if b.Flags&BindEnsure != 0 {
|
||||
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
|
||||
if pathname, err := k.evalSymlinks(b.Source.String()); err != nil {
|
||||
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
|
||||
// leave sourceFinal as nil
|
||||
return nil
|
||||
}
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
b.sourceFinal, err = NewAbs(pathname)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
if b.sourceFinal == nil {
|
||||
if b.Flags&BindOptional == 0 {
|
||||
// unreachable
|
||||
return msg.WrapErr(os.ErrClosed, "impossible bind state reached")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
source := toHost(b.sourceFinal.String())
|
||||
target := toSysroot(b.Target.String())
|
||||
|
||||
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
|
||||
// op->perms which is never set for any bind setup op so always results in 0700
|
||||
if fi, err := k.stat(source); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if fi.IsDir() {
|
||||
if err = k.mkdirAll(target, 0700); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
} else if err = k.ensureFile(target, 0444, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var flags uintptr = syscall.MS_REC
|
||||
if b.Flags&BindWritable == 0 {
|
||||
flags |= syscall.MS_RDONLY
|
||||
}
|
||||
if b.Flags&BindDevice == 0 {
|
||||
flags |= syscall.MS_NODEV
|
||||
}
|
||||
|
||||
return k.bindMount(source, target, flags, b.sourceFinal == b.Target)
|
||||
}
|
||||
|
||||
func (b *BindMountOp) Is(op Op) bool {
|
||||
vb, ok := op.(*BindMountOp)
|
||||
return ok && b.Valid() && vb.Valid() &&
|
||||
b.Source.Is(vb.Source) &&
|
||||
b.Target.Is(vb.Target) &&
|
||||
b.Flags == vb.Flags
|
||||
}
|
||||
func (*BindMountOp) prefix() string { return "mounting" }
|
||||
func (b *BindMountOp) String() string {
|
||||
if b.Source == nil || b.Target == nil {
|
||||
return "<invalid>"
|
||||
}
|
||||
if b.Source.String() == b.Target.String() {
|
||||
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
|
||||
}
|
||||
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags)
|
||||
}
|
||||
235
container/initbind_test.go
Normal file
235
container/initbind_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBindMountOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"ENOENT not optional", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
|
||||
}, wrapErrSelf(syscall.ENOENT), nil, nil},
|
||||
|
||||
{"skip optional", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
Flags: BindOptional,
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
|
||||
}, nil, nil, nil},
|
||||
|
||||
{"success optional", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
Flags: BindOptional,
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
||||
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"ensureFile device", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/dev/null"),
|
||||
Target: MustAbs("/dev/null"),
|
||||
Flags: BindWritable | BindDevice,
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"mkdirAll ensure", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
Flags: BindEnsure,
|
||||
}, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
|
||||
{"success ensure", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/usr/bin/"),
|
||||
Flags: BindEnsure,
|
||||
}, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, nil},
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
||||
{"mkdirAll", expectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success device ro", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/dev/null"),
|
||||
Target: MustAbs("/dev/null"),
|
||||
Flags: BindDevice,
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success device", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/dev/null"),
|
||||
Target: MustAbs("/dev/null"),
|
||||
Flags: BindWritable | BindDevice,
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"evalSymlinks", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
|
||||
{"stat", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"mkdirAll", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
||||
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"bindMount", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
||||
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"success", new(Params), &BindMountOp{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
||||
}, nil, []kexpect{
|
||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
||||
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
t.Run("unreachable", func(t *testing.T) {
|
||||
t.Run("nil sourceFinal not optional", func(t *testing.T) {
|
||||
wantErr := msg.WrapErr(os.ErrClosed, "impossible bind state reached")
|
||||
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
|
||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*BindMountOp)(nil), false},
|
||||
{"zero", new(BindMountOp), false},
|
||||
{"nil source", &BindMountOp{Target: MustAbs("/")}, false},
|
||||
{"nil target", &BindMountOp{Source: MustAbs("/")}, false},
|
||||
{"flag optional ensure", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindOptional | BindEnsure}, false},
|
||||
{"valid", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/")}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"autoetc", new(Ops).Bind(
|
||||
MustAbs("/etc/"),
|
||||
MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
0,
|
||||
), Ops{
|
||||
&BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(BindMountOp), new(BindMountOp), false},
|
||||
|
||||
{"internal ne", &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
}, &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
sourceFinal: MustAbs("/etc/"),
|
||||
}, true},
|
||||
|
||||
{"flags differs", &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
}, &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
Flags: BindOptional,
|
||||
}, false},
|
||||
|
||||
{"source differs", &BindMountOp{
|
||||
Source: MustAbs("/.hakurei/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
}, &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
}, false},
|
||||
|
||||
{"target differs", &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
}, &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/"),
|
||||
}, false},
|
||||
|
||||
{"equals", &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
}, &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"invalid", new(BindMountOp), "mounting", "<invalid>"},
|
||||
|
||||
{"autoetc", &BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
}, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`},
|
||||
|
||||
{"hostdev", &BindMountOp{
|
||||
Source: MustAbs("/dev/"),
|
||||
Target: MustAbs("/dev/"),
|
||||
Flags: BindWritable | BindDevice,
|
||||
}, "mounting", `"/dev/" flags 0x6`},
|
||||
})
|
||||
}
|
||||
137
container/initdev.go
Normal file
137
container/initdev.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"path"
|
||||
. "syscall"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(MountDevOp)) }
|
||||
|
||||
// Dev appends an [Op] that mounts a subset of host /dev.
|
||||
func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops {
|
||||
*f = append(*f, &MountDevOp{target, mqueue, false})
|
||||
return f
|
||||
}
|
||||
|
||||
// DevWritable appends an [Op] that mounts a writable subset of host /dev.
|
||||
// There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp].
|
||||
func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
|
||||
*f = append(*f, &MountDevOp{target, mqueue, true})
|
||||
return f
|
||||
}
|
||||
|
||||
// MountDevOp mounts a subset of host /dev on container path Target.
|
||||
// If Mqueue is true, a private instance of [FstypeMqueue] is mounted.
|
||||
// If Write is true, the resulting mount point is left writable.
|
||||
type MountDevOp struct {
|
||||
Target *Absolute
|
||||
Mqueue bool
|
||||
Write bool
|
||||
}
|
||||
|
||||
func (d *MountDevOp) Valid() bool { return d != nil && d.Target != nil }
|
||||
func (d *MountDevOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
target := toSysroot(d.Target.String())
|
||||
|
||||
if err := k.mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
|
||||
targetPath := path.Join(target, name)
|
||||
if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.bindMount(
|
||||
toHost(FHSDev+name),
|
||||
targetPath,
|
||||
0,
|
||||
true,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i, name := range []string{"stdin", "stdout", "stderr"} {
|
||||
if err := k.symlink(
|
||||
FHSProc+"self/fd/"+string(rune(i+'0')),
|
||||
path.Join(target, name),
|
||||
); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
for _, pair := range [][2]string{
|
||||
{FHSProc + "self/fd", "fd"},
|
||||
{FHSProc + "kcore", "core"},
|
||||
{"pts/ptmx", "ptmx"},
|
||||
} {
|
||||
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
|
||||
devPtsPath := path.Join(target, "pts")
|
||||
for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
|
||||
if err := k.mkdir(name, state.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
|
||||
"newinstance,ptmxmode=0666,mode=620"); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
|
||||
}
|
||||
|
||||
if state.RetainSession {
|
||||
if k.isatty(Stdout) {
|
||||
consolePath := path.Join(target, "console")
|
||||
if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if name, err := k.readlink(hostProc.stdout()); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if err = k.bindMount(
|
||||
toHost(name),
|
||||
consolePath,
|
||||
0,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d.Mqueue {
|
||||
mqueueTarget := path.Join(target, "mqueue")
|
||||
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
|
||||
return wrapErrSuffix(err, "cannot mount mqueue:")
|
||||
}
|
||||
}
|
||||
|
||||
if d.Write {
|
||||
return nil
|
||||
}
|
||||
return wrapErrSuffix(k.remount(target, MS_RDONLY),
|
||||
fmt.Sprintf("cannot remount %q:", target))
|
||||
}
|
||||
|
||||
func (d *MountDevOp) Is(op Op) bool {
|
||||
vd, ok := op.(*MountDevOp)
|
||||
return ok && d.Valid() && vd.Valid() &&
|
||||
d.Target.Is(vd.Target) &&
|
||||
d.Mqueue == vd.Mqueue &&
|
||||
d.Write == vd.Write
|
||||
}
|
||||
func (*MountDevOp) prefix() string { return "mounting" }
|
||||
func (d *MountDevOp) String() string {
|
||||
if d.Mqueue {
|
||||
return fmt.Sprintf("dev on %q with mqueue", d.Target)
|
||||
}
|
||||
return fmt.Sprintf("dev on %q", d.Target)
|
||||
}
|
||||
789
container/initdev_test.go
Normal file
789
container/initdev_test.go
Normal file
@@ -0,0 +1,789 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMountDevOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"ensureFile null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"bindMount null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"ensureFile zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"bindMount zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"ensureFile full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"bindMount full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"ensureFile random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"bindMount random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"ensureFile urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"bindMount urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"ensureFile tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"bindMount tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"symlink stdin", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"symlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"symlink stderr", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"symlink fd", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"symlink kcore", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"symlink ptmx", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"mkdir shm", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"mkdir devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"mount devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, errUnique},
|
||||
}, wrapErrSuffix(errUnique, `cannot mount devpts on "/sysroot/dev/pts":`)},
|
||||
|
||||
{"ensureFile stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, true, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"readlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, true, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "", errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"bindMount stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, true, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
|
||||
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"mkdir mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, true, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
|
||||
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"mount mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, true, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
|
||||
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, errUnique},
|
||||
}, wrapErrSuffix(errUnique, "cannot mount mqueue:")},
|
||||
|
||||
{"success no session", &Params{ParentPerm: 0755}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
Write: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0755)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0755)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0755)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0755)}, nil, nil},
|
||||
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success no tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
Write: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, false, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success no mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, true, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
|
||||
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
|
||||
{"remount", expectArgs{"/sysroot/dev", uintptr(1)}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success rw", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
Write: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, true, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
|
||||
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
|
||||
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
|
||||
{"isatty", expectArgs{1}, true, nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
|
||||
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
|
||||
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil},
|
||||
{"remount", expectArgs{"/sysroot/dev", uintptr(1)}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*MountDevOp)(nil), false},
|
||||
{"zero", new(MountDevOp), false},
|
||||
{"valid", &MountDevOp{Target: MustAbs("/dev/")}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"dev", new(Ops).Dev(MustAbs("/dev/"), true), Ops{
|
||||
&MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
},
|
||||
}},
|
||||
|
||||
{"dev writable", new(Ops).DevWritable(MustAbs("/.hakurei/dev/"), false), Ops{
|
||||
&MountDevOp{
|
||||
Target: MustAbs("/.hakurei/dev/"),
|
||||
Write: true,
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(MountDevOp), new(MountDevOp), false},
|
||||
|
||||
{"write differs", &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
Write: true,
|
||||
}, false},
|
||||
|
||||
{"mqueue differs", &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, false},
|
||||
|
||||
{"target differs", &MountDevOp{
|
||||
Target: MustAbs("/"),
|
||||
Mqueue: true,
|
||||
}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, false},
|
||||
|
||||
{"equals", &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"mqueue", &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Mqueue: true,
|
||||
}, "mounting", `dev on "/dev/" with mqueue`},
|
||||
|
||||
{"dev", &MountDevOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
}, "mounting", `dev on "/dev/"`},
|
||||
})
|
||||
}
|
||||
36
container/initmkdir.go
Normal file
36
container/initmkdir.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(MkdirOp)) }
|
||||
|
||||
// Mkdir appends an [Op] that creates a directory in the container filesystem.
|
||||
func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops {
|
||||
*f = append(*f, &MkdirOp{name, perm})
|
||||
return f
|
||||
}
|
||||
|
||||
// MkdirOp creates a directory at container Path with permission bits set to Perm.
|
||||
type MkdirOp struct {
|
||||
Path *Absolute
|
||||
Perm os.FileMode
|
||||
}
|
||||
|
||||
func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil }
|
||||
func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
return wrapErrSelf(k.mkdirAll(toSysroot(m.Path.String()), m.Perm))
|
||||
}
|
||||
|
||||
func (m *MkdirOp) Is(op Op) bool {
|
||||
vm, ok := op.(*MkdirOp)
|
||||
return ok && m.Valid() && vm.Valid() &&
|
||||
m.Path.Is(vm.Path) &&
|
||||
m.Perm == vm.Perm
|
||||
}
|
||||
func (*MkdirOp) prefix() string { return "creating" }
|
||||
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
||||
42
container/initmkdir_test.go
Normal file
42
container/initmkdir_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMkdirOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"success", new(Params), &MkdirOp{
|
||||
Path: MustAbs("/.hakurei"),
|
||||
Perm: 0500,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*MkdirOp)(nil), false},
|
||||
{"zero", new(MkdirOp), false},
|
||||
{"valid", &MkdirOp{Path: MustAbs("/.hakurei")}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"etc", new(Ops).Mkdir(MustAbs("/etc/"), 0), Ops{
|
||||
&MkdirOp{Path: MustAbs("/etc/")},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(MkdirOp), new(MkdirOp), false},
|
||||
{"path differs", &MkdirOp{Path: MustAbs("/"), Perm: 0755}, &MkdirOp{Path: MustAbs("/etc/"), Perm: 0755}, false},
|
||||
{"perm differs", &MkdirOp{Path: MustAbs("/")}, &MkdirOp{Path: MustAbs("/"), Perm: 0755}, false},
|
||||
{"equals", &MkdirOp{Path: MustAbs("/")}, &MkdirOp{Path: MustAbs("/")}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"etc", &MkdirOp{
|
||||
Path: MustAbs("/etc/"),
|
||||
}, "creating", `directory "/etc/" perm ----------`},
|
||||
})
|
||||
}
|
||||
184
container/initoverlay.go
Normal file
184
container/initoverlay.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// intermediate root file name pattern for [MountOverlayOp.Upper];
|
||||
// remains after apply returns
|
||||
intermediatePatternOverlayUpper = "overlay.upper.*"
|
||||
// intermediate root file name pattern for [MountOverlayOp.Work];
|
||||
// remains after apply returns
|
||||
intermediatePatternOverlayWork = "overlay.work.*"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(MountOverlayOp)) }
|
||||
|
||||
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
|
||||
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
|
||||
*f = append(*f, &MountOverlayOp{
|
||||
Target: target,
|
||||
Lower: layers,
|
||||
Upper: state,
|
||||
Work: work,
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
// OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]
|
||||
// with an ephemeral upperdir and workdir.
|
||||
func (f *Ops) OverlayEphemeral(target *Absolute, layers ...*Absolute) *Ops {
|
||||
return f.Overlay(target, AbsFHSRoot, nil, layers...)
|
||||
}
|
||||
|
||||
// OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
|
||||
func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops {
|
||||
return f.Overlay(target, nil, nil, layers...)
|
||||
}
|
||||
|
||||
// MountOverlayOp mounts [FstypeOverlay] on container path Target.
|
||||
type MountOverlayOp struct {
|
||||
Target *Absolute
|
||||
|
||||
// Any filesystem, does not need to be on a writable filesystem.
|
||||
Lower []*Absolute
|
||||
// formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early
|
||||
lower []string
|
||||
// The upperdir is normally on a writable filesystem.
|
||||
//
|
||||
// If Work is nil and Upper holds the special value [AbsFHSRoot],
|
||||
// an ephemeral upperdir and workdir will be set up.
|
||||
//
|
||||
// If both Work and Upper are nil, upperdir and workdir is omitted and the overlay is mounted readonly.
|
||||
Upper *Absolute
|
||||
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
|
||||
upper string
|
||||
// The workdir needs to be an empty directory on the same filesystem as upperdir.
|
||||
Work *Absolute
|
||||
// formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early
|
||||
work string
|
||||
|
||||
ephemeral bool
|
||||
|
||||
// used internally for mounting to the intermediate root
|
||||
noPrefix bool
|
||||
}
|
||||
|
||||
func (o *MountOverlayOp) Valid() bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
if o.Work != nil && o.Upper == nil {
|
||||
return false
|
||||
}
|
||||
if slices.Contains(o.Lower, nil) {
|
||||
return false
|
||||
}
|
||||
return o.Target != nil
|
||||
}
|
||||
|
||||
func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
if o.Work == nil && o.Upper != nil {
|
||||
switch o.Upper.String() {
|
||||
case FHSRoot: // ephemeral
|
||||
o.ephemeral = true // intermediate root not yet available
|
||||
|
||||
default:
|
||||
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("upperdir has unexpected value %q", o.Upper))
|
||||
}
|
||||
}
|
||||
// readonly handled in apply
|
||||
|
||||
if !o.ephemeral {
|
||||
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
|
||||
// unreachable
|
||||
return msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
|
||||
}
|
||||
|
||||
if o.Upper != nil {
|
||||
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
o.upper = EscapeOverlayDataSegment(toHost(v))
|
||||
}
|
||||
}
|
||||
|
||||
if o.Work != nil {
|
||||
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
o.work = EscapeOverlayDataSegment(toHost(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
o.lower = make([]string, len(o.Lower))
|
||||
for i, a := range o.Lower { // nil checked in Valid
|
||||
if v, err := k.evalSymlinks(a.String()); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
target := o.Target.String()
|
||||
if !o.noPrefix {
|
||||
target = toSysroot(target)
|
||||
}
|
||||
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
|
||||
if o.ephemeral {
|
||||
var err error
|
||||
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
|
||||
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
|
||||
options := make([]string, 0, 4)
|
||||
|
||||
if o.upper == zeroString && o.work == zeroString { // readonly
|
||||
if len(o.Lower) < 2 {
|
||||
return msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")
|
||||
}
|
||||
// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
|
||||
} else {
|
||||
if len(o.Lower) == 0 {
|
||||
return msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")
|
||||
}
|
||||
options = append(options,
|
||||
OptionOverlayUpperdir+"="+o.upper,
|
||||
OptionOverlayWorkdir+"="+o.work)
|
||||
}
|
||||
options = append(options,
|
||||
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
|
||||
OptionOverlayUserxattr)
|
||||
|
||||
return wrapErrSuffix(k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)),
|
||||
fmt.Sprintf("cannot mount overlay on %q:", o.Target))
|
||||
}
|
||||
|
||||
func (o *MountOverlayOp) Is(op Op) bool {
|
||||
vo, ok := op.(*MountOverlayOp)
|
||||
return ok && o.Valid() && vo.Valid() &&
|
||||
o.Target.Is(vo.Target) &&
|
||||
slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) &&
|
||||
o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
|
||||
}
|
||||
func (*MountOverlayOp) prefix() string { return "mounting" }
|
||||
func (o *MountOverlayOp) String() string {
|
||||
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
|
||||
}
|
||||
368
container/initoverlay_test.go
Normal file
368
container/initoverlay_test.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMountOverlayOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
||||
Target: MustAbs("/"),
|
||||
Lower: []*Absolute{
|
||||
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
||||
},
|
||||
Upper: MustAbs("/proc/"),
|
||||
}, nil, msg.WrapErr(fs.ErrInvalid, `upperdir has unexpected value "/proc/"`), nil, nil},
|
||||
|
||||
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
||||
Target: MustAbs("/"),
|
||||
Lower: []*Absolute{
|
||||
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
||||
},
|
||||
Upper: MustAbs("/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
|
||||
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
||||
Target: MustAbs("/"),
|
||||
Lower: []*Absolute{
|
||||
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
||||
},
|
||||
Upper: MustAbs("/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
|
||||
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
|
||||
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
||||
Target: MustAbs("/"),
|
||||
Lower: []*Absolute{
|
||||
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
||||
},
|
||||
Upper: MustAbs("/"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
|
||||
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
|
||||
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil},
|
||||
{"mount", expectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
|
||||
"upperdir=overlay.upper.32768," +
|
||||
"workdir=overlay.work.32768," +
|
||||
"lowerdir=" +
|
||||
`/host/var/lib/planterette/base/debian\:f92c9052:` +
|
||||
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
|
||||
"userxattr"}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{
|
||||
MustAbs("/mnt-root/nix/.ro-store"),
|
||||
},
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
|
||||
}, msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")},
|
||||
|
||||
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{
|
||||
MustAbs("/mnt-root/nix/.ro-store"),
|
||||
MustAbs("/mnt-root/nix/.ro-store0"),
|
||||
},
|
||||
noPrefix: true,
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/nix/store", os.FileMode(0755)}, nil, nil},
|
||||
{"mount", expectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
|
||||
"lowerdir=" +
|
||||
"/host/mnt-root/nix/.ro-store:" +
|
||||
"/host/mnt-root/nix/.ro-store0," +
|
||||
"userxattr"}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{
|
||||
MustAbs("/mnt-root/nix/.ro-store"),
|
||||
MustAbs("/mnt-root/nix/.ro-store0"),
|
||||
},
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
|
||||
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||
"lowerdir=" +
|
||||
"/host/mnt-root/nix/.ro-store:" +
|
||||
"/host/mnt-root/nix/.ro-store0," +
|
||||
"userxattr"}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
|
||||
}, msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")},
|
||||
|
||||
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
|
||||
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
|
||||
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
|
||||
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
|
||||
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, errUnique},
|
||||
}, wrapErrSuffix(errUnique, `cannot mount overlay on "/nix/store":`)},
|
||||
|
||||
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
|
||||
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
||||
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
||||
"lowerdir=/host/mnt-root/nix/ro-store," +
|
||||
"userxattr"}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{
|
||||
MustAbs("/mnt-root/nix/.ro-store"),
|
||||
MustAbs("/mnt-root/nix/.ro-store0"),
|
||||
MustAbs("/mnt-root/nix/.ro-store1"),
|
||||
MustAbs("/mnt-root/nix/.ro-store2"),
|
||||
MustAbs("/mnt-root/nix/.ro-store3"),
|
||||
},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, []kexpect{
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil},
|
||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
|
||||
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
||||
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
||||
"lowerdir=" +
|
||||
"/host/mnt-root/nix/ro-store:" +
|
||||
"/host/mnt-root/nix/ro-store0:" +
|
||||
"/host/mnt-root/nix/ro-store1:" +
|
||||
"/host/mnt-root/nix/ro-store2:" +
|
||||
"/host/mnt-root/nix/ro-store3," +
|
||||
"userxattr"}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
t.Run("unreachable", func(t *testing.T) {
|
||||
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
|
||||
wantErr := msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
|
||||
if err := (&MountOverlayOp{
|
||||
Work: MustAbs("/"),
|
||||
}).early(nil, nil); !errors.Is(err, wantErr) {
|
||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*MountOverlayOp)(nil), false},
|
||||
{"zero", new(MountOverlayOp), false},
|
||||
{"nil lower", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{nil}}, false},
|
||||
{"ro", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}}, true},
|
||||
{"ro work", &MountOverlayOp{Target: MustAbs("/"), Work: MustAbs("/tmp/")}, false},
|
||||
{"rw", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}, Upper: MustAbs("/"), Work: MustAbs("/")}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"full", new(Ops).Overlay(
|
||||
MustAbs("/nix/store"),
|
||||
MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
MustAbs("/mnt-root/nix/.ro-store"),
|
||||
), Ops{
|
||||
&MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
},
|
||||
}},
|
||||
|
||||
{"ephemeral", new(Ops).OverlayEphemeral(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
|
||||
&MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/"),
|
||||
},
|
||||
}},
|
||||
|
||||
{"readonly", new(Ops).OverlayReadonly(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
|
||||
&MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(MountOverlayOp), new(MountOverlayOp), false},
|
||||
|
||||
{"differs target", &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store/differs"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
|
||||
|
||||
{"differs lower", &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store/differs")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
|
||||
|
||||
{"differs upper", &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper/differs"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
|
||||
|
||||
{"differs work", &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work/differs"),
|
||||
}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
|
||||
|
||||
{"equals ro", &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}}, true},
|
||||
|
||||
{"equals", &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"nix", &MountOverlayOp{
|
||||
Target: MustAbs("/nix/store"),
|
||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||
}, "mounting", `overlay on "/nix/store" with 1 layers`},
|
||||
})
|
||||
}
|
||||
78
container/initplace.go
Normal file
78
container/initplace.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
// intermediate root file name pattern for [TmpfileOp]
|
||||
intermediatePatternTmpfile = "tmp.*"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(TmpfileOp)) }
|
||||
|
||||
// Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data].
|
||||
func (f *Ops) Place(name *Absolute, data []byte) *Ops {
|
||||
*f = append(*f, &TmpfileOp{name, data})
|
||||
return f
|
||||
}
|
||||
|
||||
// PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to.
|
||||
func (f *Ops) PlaceP(name *Absolute, dataP **[]byte) *Ops {
|
||||
t := &TmpfileOp{Path: name}
|
||||
*dataP = &t.Data
|
||||
|
||||
*f = append(*f, t)
|
||||
return f
|
||||
}
|
||||
|
||||
// TmpfileOp places a file on container Path containing Data.
|
||||
type TmpfileOp struct {
|
||||
Path *Absolute
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (t *TmpfileOp) Valid() bool { return t != nil && t.Path != nil }
|
||||
func (t *TmpfileOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
var tmpPath string
|
||||
if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if _, err = f.Write(t.Data); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot write to intermediate file:")
|
||||
} else if err = f.Close(); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot close intermediate file:")
|
||||
} else {
|
||||
tmpPath = f.Name()
|
||||
}
|
||||
|
||||
target := toSysroot(t.Path.String())
|
||||
if err := k.ensureFile(target, 0444, state.ParentPerm); err != nil {
|
||||
return err
|
||||
} else if err = k.bindMount(
|
||||
tmpPath,
|
||||
target,
|
||||
syscall.MS_RDONLY|syscall.MS_NODEV,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
} else if err = k.remove(tmpPath); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TmpfileOp) Is(op Op) bool {
|
||||
vt, ok := op.(*TmpfileOp)
|
||||
return ok && t.Valid() && vt.Valid() &&
|
||||
t.Path.Is(vt.Path) &&
|
||||
string(t.Data) == string(vt.Data)
|
||||
}
|
||||
func (*TmpfileOp) prefix() string { return "placing" }
|
||||
func (t *TmpfileOp) String() string {
|
||||
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
|
||||
}
|
||||
131
container/initplace_test.go
Normal file
131
container/initplace_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTmpfileOp(t *testing.T) {
|
||||
const sampleDataString = `chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`
|
||||
var (
|
||||
samplePath = MustAbs("/etc/passwd")
|
||||
sampleData = []byte(sampleDataString)
|
||||
)
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, nil, nil, []kexpect{
|
||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, nil, nil, []kexpect{
|
||||
{"createTemp", expectArgs{"/", "tmp.*"}, writeErrOsFile{errUnique}, nil},
|
||||
}, wrapErrSuffix(errUnique, "cannot write to intermediate file:")},
|
||||
|
||||
{"Close", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, nil, nil, []kexpect{
|
||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, errUnique), nil},
|
||||
}, wrapErrSuffix(errUnique, "cannot close intermediate file:")},
|
||||
|
||||
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, nil, nil, []kexpect{
|
||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, nil, nil, []kexpect{
|
||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, errUnique},
|
||||
}, errUnique},
|
||||
|
||||
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, nil, nil, []kexpect{
|
||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
|
||||
{"remove", expectArgs{"tmp.32768"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, nil, nil, []kexpect{
|
||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
|
||||
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
||||
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
|
||||
{"remove", expectArgs{"tmp.32768"}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*TmpfileOp)(nil), false},
|
||||
{"zero", new(TmpfileOp), false},
|
||||
{"valid", &TmpfileOp{Path: samplePath}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"noref", new(Ops).Place(samplePath, sampleData), Ops{
|
||||
&TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
},
|
||||
}},
|
||||
|
||||
{"ref", new(Ops).PlaceP(samplePath, new(*[]byte)), Ops{
|
||||
&TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: []byte{},
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(TmpfileOp), new(TmpfileOp), false},
|
||||
|
||||
{"differs path", &TmpfileOp{
|
||||
Path: MustAbs("/etc/group"),
|
||||
Data: sampleData,
|
||||
}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, false},
|
||||
|
||||
{"differs data", &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: append(sampleData, 0),
|
||||
}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, false},
|
||||
|
||||
{"equals", &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"passwd", &TmpfileOp{
|
||||
Path: samplePath,
|
||||
Data: sampleData,
|
||||
}, "placing", `tmpfile "/etc/passwd" (49 bytes)`},
|
||||
})
|
||||
}
|
||||
37
container/initproc.go
Normal file
37
container/initproc.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
. "syscall"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(MountProcOp)) }
|
||||
|
||||
// Proc appends an [Op] that mounts a private instance of proc.
|
||||
func (f *Ops) Proc(target *Absolute) *Ops {
|
||||
*f = append(*f, &MountProcOp{target})
|
||||
return f
|
||||
}
|
||||
|
||||
// MountProcOp mounts a new instance of [FstypeProc] on container path Target.
|
||||
type MountProcOp struct{ Target *Absolute }
|
||||
|
||||
func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil }
|
||||
func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
target := toSysroot(p.Target.String())
|
||||
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return wrapErrSuffix(k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString),
|
||||
fmt.Sprintf("cannot mount proc on %q:", p.Target.String()))
|
||||
}
|
||||
|
||||
func (p *MountProcOp) Is(op Op) bool {
|
||||
vp, ok := op.(*MountProcOp)
|
||||
return ok && p.Valid() && vp.Valid() &&
|
||||
p.Target.Is(vp.Target)
|
||||
}
|
||||
func (*MountProcOp) prefix() string { return "mounting" }
|
||||
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
|
||||
58
container/initproc_test.go
Normal file
58
container/initproc_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMountProcOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"mkdir", &Params{ParentPerm: 0755},
|
||||
&MountProcOp{
|
||||
Target: MustAbs("/proc/"),
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"success", &Params{ParentPerm: 0700},
|
||||
&MountProcOp{
|
||||
Target: MustAbs("/proc/"),
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil},
|
||||
{"mount", expectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*MountProcOp)(nil), false},
|
||||
{"zero", new(MountProcOp), false},
|
||||
{"valid", &MountProcOp{Target: MustAbs("/proc/")}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"proc", new(Ops).Proc(MustAbs("/proc/")), Ops{
|
||||
&MountProcOp{Target: MustAbs("/proc/")},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(MountProcOp), new(MountProcOp), false},
|
||||
|
||||
{"target differs", &MountProcOp{
|
||||
Target: MustAbs("/proc/nonexistent"),
|
||||
}, &MountProcOp{
|
||||
Target: MustAbs("/proc/"),
|
||||
}, false},
|
||||
|
||||
{"equals", &MountProcOp{
|
||||
Target: MustAbs("/proc/"),
|
||||
}, &MountProcOp{
|
||||
Target: MustAbs("/proc/"),
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"proc", &MountProcOp{Target: MustAbs("/proc/")},
|
||||
"mounting", `proc on "/proc/"`},
|
||||
})
|
||||
}
|
||||
36
container/initremount.go
Normal file
36
container/initremount.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(RemountOp)) }
|
||||
|
||||
// Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target].
|
||||
func (f *Ops) Remount(target *Absolute, flags uintptr) *Ops {
|
||||
*f = append(*f, &RemountOp{target, flags})
|
||||
return f
|
||||
}
|
||||
|
||||
// RemountOp remounts Target with Flags.
|
||||
type RemountOp struct {
|
||||
Target *Absolute
|
||||
Flags uintptr
|
||||
}
|
||||
|
||||
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
|
||||
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
return wrapErrSuffix(k.remount(toSysroot(r.Target.String()), r.Flags),
|
||||
fmt.Sprintf("cannot remount %q:", r.Target))
|
||||
}
|
||||
|
||||
func (r *RemountOp) Is(op Op) bool {
|
||||
vr, ok := op.(*RemountOp)
|
||||
return ok && r.Valid() && vr.Valid() &&
|
||||
r.Target.Is(vr.Target) &&
|
||||
r.Flags == vr.Flags
|
||||
}
|
||||
func (*RemountOp) prefix() string { return "remounting" }
|
||||
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
|
||||
67
container/initremount_test.go
Normal file
67
container/initremount_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRemountOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"success", new(Params), &RemountOp{
|
||||
Target: MustAbs("/"),
|
||||
Flags: syscall.MS_RDONLY,
|
||||
}, nil, nil, []kexpect{
|
||||
{"remount", expectArgs{"/sysroot", uintptr(1)}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*RemountOp)(nil), false},
|
||||
{"zero", new(RemountOp), false},
|
||||
{"valid", &RemountOp{Target: MustAbs("/"), Flags: syscall.MS_RDONLY}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"root", new(Ops).Remount(MustAbs("/"), syscall.MS_RDONLY), Ops{
|
||||
&RemountOp{
|
||||
Target: MustAbs("/"),
|
||||
Flags: syscall.MS_RDONLY,
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(RemountOp), new(RemountOp), false},
|
||||
|
||||
{"target differs", &RemountOp{
|
||||
Target: MustAbs("/dev/"),
|
||||
Flags: syscall.MS_RDONLY,
|
||||
}, &RemountOp{
|
||||
Target: MustAbs("/"),
|
||||
Flags: syscall.MS_RDONLY,
|
||||
}, false},
|
||||
|
||||
{"flags differs", &RemountOp{
|
||||
Target: MustAbs("/"),
|
||||
Flags: syscall.MS_RDONLY | syscall.MS_NODEV,
|
||||
}, &RemountOp{
|
||||
Target: MustAbs("/"),
|
||||
Flags: syscall.MS_RDONLY,
|
||||
}, false},
|
||||
|
||||
{"equals", &RemountOp{
|
||||
Target: MustAbs("/"),
|
||||
Flags: syscall.MS_RDONLY,
|
||||
}, &RemountOp{
|
||||
Target: MustAbs("/"),
|
||||
Flags: syscall.MS_RDONLY,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"root", &RemountOp{
|
||||
Target: MustAbs("/"),
|
||||
Flags: syscall.MS_RDONLY,
|
||||
}, "remounting", `"/" flags 0x1`},
|
||||
})
|
||||
}
|
||||
62
container/initsymlink.go
Normal file
62
container/initsymlink.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(SymlinkOp)) }
|
||||
|
||||
// Link appends an [Op] that creates a symlink in the container filesystem.
|
||||
func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops {
|
||||
*f = append(*f, &SymlinkOp{target, linkName, dereference})
|
||||
return f
|
||||
}
|
||||
|
||||
// SymlinkOp optionally dereferences LinkName and creates a symlink at container path Target.
|
||||
type SymlinkOp struct {
|
||||
Target *Absolute
|
||||
// LinkName is an arbitrary uninterpreted pathname.
|
||||
LinkName string
|
||||
|
||||
// Dereference causes LinkName to be dereferenced during early.
|
||||
Dereference bool
|
||||
}
|
||||
|
||||
func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkName != zeroString }
|
||||
|
||||
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
if l.Dereference {
|
||||
if !isAbs(l.LinkName) {
|
||||
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("path %q is not absolute", l.LinkName))
|
||||
}
|
||||
if name, err := k.readlink(l.LinkName); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
l.LinkName = name
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
target := toSysroot(l.Target.String())
|
||||
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return wrapErrSelf(k.symlink(l.LinkName, target))
|
||||
}
|
||||
|
||||
func (l *SymlinkOp) Is(op Op) bool {
|
||||
vl, ok := op.(*SymlinkOp)
|
||||
return ok && l.Valid() && vl.Valid() &&
|
||||
l.Target.Is(vl.Target) &&
|
||||
l.LinkName == vl.LinkName &&
|
||||
l.Dereference == vl.Dereference
|
||||
}
|
||||
func (*SymlinkOp) prefix() string { return "creating" }
|
||||
func (l *SymlinkOp) String() string {
|
||||
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
|
||||
}
|
||||
124
container/initsymlink_test.go
Normal file
124
container/initsymlink_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSymlinkOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
|
||||
Target: MustAbs("/etc/nixos"),
|
||||
LinkName: "/etc/static/nixos",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||
Target: MustAbs("/etc/mtab"),
|
||||
LinkName: "etc/mtab",
|
||||
Dereference: true,
|
||||
}, nil, msg.WrapErr(fs.ErrInvalid, `path "etc/mtab" is not absolute`), nil, nil},
|
||||
|
||||
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||
Target: MustAbs("/etc/mtab"),
|
||||
LinkName: "/etc/mtab",
|
||||
Dereference: true,
|
||||
}, []kexpect{
|
||||
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
|
||||
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
|
||||
Target: MustAbs("/etc/nixos"),
|
||||
LinkName: "/etc/static/nixos",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil},
|
||||
{"symlink", expectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil},
|
||||
}, nil},
|
||||
|
||||
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||
Target: MustAbs("/etc/mtab"),
|
||||
LinkName: "/etc/mtab",
|
||||
Dereference: true,
|
||||
}, []kexpect{
|
||||
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", nil},
|
||||
}, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*SymlinkOp)(nil), false},
|
||||
{"zero", new(SymlinkOp), false},
|
||||
{"nil target", &SymlinkOp{LinkName: "/run/current-system"}, false},
|
||||
{"zero linkname", &SymlinkOp{Target: MustAbs("/run/current-system")}, false},
|
||||
{"valid", &SymlinkOp{Target: MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"current-system", new(Ops).Link(
|
||||
MustAbs("/run/current-system"),
|
||||
"/run/current-system",
|
||||
true,
|
||||
), Ops{
|
||||
&SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system",
|
||||
Dereference: true,
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(SymlinkOp), new(SymlinkOp), false},
|
||||
|
||||
{"target differs", &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system/differs"),
|
||||
LinkName: "/run/current-system",
|
||||
Dereference: true,
|
||||
}, &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system",
|
||||
Dereference: true,
|
||||
}, false},
|
||||
|
||||
{"linkname differs", &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system/differs",
|
||||
Dereference: true,
|
||||
}, &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system",
|
||||
Dereference: true,
|
||||
}, false},
|
||||
|
||||
{"dereference differs", &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system",
|
||||
}, &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system",
|
||||
Dereference: true,
|
||||
}, false},
|
||||
|
||||
{"equals", &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system",
|
||||
Dereference: true,
|
||||
}, &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system",
|
||||
Dereference: true,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"current-system", &SymlinkOp{
|
||||
Target: MustAbs("/run/current-system"),
|
||||
LinkName: "/run/current-system",
|
||||
Dereference: true,
|
||||
}, "creating", `symlink on "/run/current-system" linkname "/run/current-system"`},
|
||||
})
|
||||
}
|
||||
54
container/inittmpfs.go
Normal file
54
container/inittmpfs.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math"
|
||||
"os"
|
||||
. "syscall"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(MountTmpfsOp)) }
|
||||
|
||||
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
|
||||
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
|
||||
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
|
||||
return f
|
||||
}
|
||||
|
||||
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path].
|
||||
func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
|
||||
*f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
|
||||
return f
|
||||
}
|
||||
|
||||
// MountTmpfsOp mounts [FstypeTmpfs] on container Path.
|
||||
type MountTmpfsOp struct {
|
||||
FSName string
|
||||
Path *Absolute
|
||||
Flags uintptr
|
||||
Size int
|
||||
Perm os.FileMode
|
||||
}
|
||||
|
||||
func (t *MountTmpfsOp) Valid() bool { return t != nil && t.Path != nil && t.FSName != zeroString }
|
||||
func (t *MountTmpfsOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
if t.Size < 0 || t.Size > math.MaxUint>>1 {
|
||||
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("size %d out of bounds", t.Size))
|
||||
}
|
||||
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
|
||||
}
|
||||
|
||||
func (t *MountTmpfsOp) Is(op Op) bool {
|
||||
vt, ok := op.(*MountTmpfsOp)
|
||||
return ok && t.Valid() && vt.Valid() &&
|
||||
t.FSName == vt.FSName &&
|
||||
t.Path.Is(vt.Path) &&
|
||||
t.Flags == vt.Flags &&
|
||||
t.Size == vt.Size &&
|
||||
t.Perm == vt.Perm
|
||||
}
|
||||
func (*MountTmpfsOp) prefix() string { return "mounting" }
|
||||
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||
165
container/inittmpfs_test.go
Normal file
165
container/inittmpfs_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMountTmpfsOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"size oob", new(Params), &MountTmpfsOp{
|
||||
Size: -1,
|
||||
}, nil, nil, nil, msg.WrapErr(fs.ErrInvalid, "size -1 out of bounds")},
|
||||
|
||||
{"success", new(Params), &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user/1000/"),
|
||||
Size: 1 << 10,
|
||||
Perm: 0700,
|
||||
}, nil, nil, []kexpect{
|
||||
{"mountTmpfs", expectArgs{
|
||||
"ephemeral", // fsname
|
||||
"/sysroot/run/user/1000", // target
|
||||
uintptr(0), // flags
|
||||
0x400, // size
|
||||
os.FileMode(0700), // perm
|
||||
}, nil, nil},
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*MountTmpfsOp)(nil), false},
|
||||
{"zero", new(MountTmpfsOp), false},
|
||||
{"nil path", &MountTmpfsOp{FSName: "tmpfs"}, false},
|
||||
{"zero fsname", &MountTmpfsOp{Path: MustAbs("/tmp/")}, false},
|
||||
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: MustAbs("/tmp/")}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"runtime", new(Ops).Tmpfs(
|
||||
MustAbs("/run/user"),
|
||||
1<<10,
|
||||
0755,
|
||||
), Ops{
|
||||
&MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
},
|
||||
}},
|
||||
|
||||
{"nscd", new(Ops).Readonly(
|
||||
MustAbs("/var/run/nscd"),
|
||||
0755,
|
||||
), Ops{
|
||||
&MountTmpfsOp{
|
||||
FSName: "readonly",
|
||||
Path: MustAbs("/var/run/nscd"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
|
||||
Perm: 0755,
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(MountTmpfsOp), new(MountTmpfsOp), false},
|
||||
|
||||
{"fsname differs", &MountTmpfsOp{
|
||||
FSName: "readonly",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, false},
|
||||
|
||||
{"path differs", &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user/differs"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, false},
|
||||
|
||||
{"flags differs", &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, false},
|
||||
|
||||
{"size differs", &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1,
|
||||
Perm: 0755,
|
||||
}, &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, false},
|
||||
|
||||
{"perm differs", &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0700,
|
||||
}, &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, false},
|
||||
|
||||
{"equals", &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"runtime", &MountTmpfsOp{
|
||||
FSName: "ephemeral",
|
||||
Path: MustAbs("/run/user"),
|
||||
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||
Size: 1 << 10,
|
||||
Perm: 0755,
|
||||
}, "mounting", `tmpfs on "/run/user" size 1024`},
|
||||
})
|
||||
}
|
||||
239
container/landlock.go
Normal file
239
container/landlock.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
// include/uapi/linux/landlock.h
|
||||
|
||||
const (
|
||||
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
|
||||
)
|
||||
|
||||
type LandlockAccessFS uintptr
|
||||
|
||||
const (
|
||||
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
|
||||
LANDLOCK_ACCESS_FS_WRITE_FILE
|
||||
LANDLOCK_ACCESS_FS_READ_FILE
|
||||
LANDLOCK_ACCESS_FS_READ_DIR
|
||||
LANDLOCK_ACCESS_FS_REMOVE_DIR
|
||||
LANDLOCK_ACCESS_FS_REMOVE_FILE
|
||||
LANDLOCK_ACCESS_FS_MAKE_CHAR
|
||||
LANDLOCK_ACCESS_FS_MAKE_DIR
|
||||
LANDLOCK_ACCESS_FS_MAKE_REG
|
||||
LANDLOCK_ACCESS_FS_MAKE_SOCK
|
||||
LANDLOCK_ACCESS_FS_MAKE_FIFO
|
||||
LANDLOCK_ACCESS_FS_MAKE_BLOCK
|
||||
LANDLOCK_ACCESS_FS_MAKE_SYM
|
||||
LANDLOCK_ACCESS_FS_REFER
|
||||
LANDLOCK_ACCESS_FS_TRUNCATE
|
||||
LANDLOCK_ACCESS_FS_IOCTL_DEV
|
||||
|
||||
_LANDLOCK_ACCESS_FS_DELIM
|
||||
)
|
||||
|
||||
func (f LandlockAccessFS) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_ACCESS_FS_EXECUTE:
|
||||
return "execute"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_WRITE_FILE:
|
||||
return "write_file"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_READ_FILE:
|
||||
return "read_file"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_READ_DIR:
|
||||
return "read_dir"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_REMOVE_DIR:
|
||||
return "remove_dir"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_REMOVE_FILE:
|
||||
return "remove_file"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_MAKE_CHAR:
|
||||
return "make_char"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_MAKE_DIR:
|
||||
return "make_dir"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_MAKE_REG:
|
||||
return "make_reg"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_MAKE_SOCK:
|
||||
return "make_sock"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_MAKE_FIFO:
|
||||
return "make_fifo"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_MAKE_BLOCK:
|
||||
return "make_block"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_MAKE_SYM:
|
||||
return "make_sym"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_REFER:
|
||||
return "fs_refer"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_TRUNCATE:
|
||||
return "fs_truncate"
|
||||
|
||||
case LANDLOCK_ACCESS_FS_IOCTL_DEV:
|
||||
return "fs_ioctl_dev"
|
||||
|
||||
default:
|
||||
var c []LandlockAccessFS
|
||||
for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
}
|
||||
if len(c) == 0 {
|
||||
return "NULL"
|
||||
}
|
||||
s := make([]string, len(c))
|
||||
for i, v := range c {
|
||||
s[i] = v.String()
|
||||
}
|
||||
return strings.Join(s, " ")
|
||||
}
|
||||
}
|
||||
|
||||
type LandlockAccessNet uintptr
|
||||
|
||||
const (
|
||||
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
|
||||
LANDLOCK_ACCESS_NET_CONNECT_TCP
|
||||
|
||||
_LANDLOCK_ACCESS_NET_DELIM
|
||||
)
|
||||
|
||||
func (f LandlockAccessNet) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_ACCESS_NET_BIND_TCP:
|
||||
return "bind_tcp"
|
||||
|
||||
case LANDLOCK_ACCESS_NET_CONNECT_TCP:
|
||||
return "connect_tcp"
|
||||
|
||||
default:
|
||||
var c []LandlockAccessNet
|
||||
for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
}
|
||||
if len(c) == 0 {
|
||||
return "NULL"
|
||||
}
|
||||
s := make([]string, len(c))
|
||||
for i, v := range c {
|
||||
s[i] = v.String()
|
||||
}
|
||||
return strings.Join(s, " ")
|
||||
}
|
||||
}
|
||||
|
||||
type LandlockScope uintptr
|
||||
|
||||
const (
|
||||
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
|
||||
LANDLOCK_SCOPE_SIGNAL
|
||||
|
||||
_LANDLOCK_SCOPE_DELIM
|
||||
)
|
||||
|
||||
func (f LandlockScope) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
|
||||
return "abstract_unix_socket"
|
||||
|
||||
case LANDLOCK_SCOPE_SIGNAL:
|
||||
return "signal"
|
||||
|
||||
default:
|
||||
var c []LandlockScope
|
||||
for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
}
|
||||
if len(c) == 0 {
|
||||
return "NULL"
|
||||
}
|
||||
s := make([]string, len(c))
|
||||
for i, v := range c {
|
||||
s[i] = v.String()
|
||||
}
|
||||
return strings.Join(s, " ")
|
||||
}
|
||||
}
|
||||
|
||||
type RulesetAttr struct {
|
||||
// Bitmask of handled filesystem actions.
|
||||
HandledAccessFS LandlockAccessFS
|
||||
// Bitmask of handled network actions.
|
||||
HandledAccessNet LandlockAccessNet
|
||||
// Bitmask of scopes restricting a Landlock domain from accessing outside resources (e.g. IPCs).
|
||||
Scoped LandlockScope
|
||||
}
|
||||
|
||||
func (rulesetAttr *RulesetAttr) String() string {
|
||||
if rulesetAttr == nil {
|
||||
return "NULL"
|
||||
}
|
||||
elems := make([]string, 0, 3)
|
||||
if rulesetAttr.HandledAccessFS > 0 {
|
||||
elems = append(elems, "fs: "+rulesetAttr.HandledAccessFS.String())
|
||||
}
|
||||
if rulesetAttr.HandledAccessNet > 0 {
|
||||
elems = append(elems, "net: "+rulesetAttr.HandledAccessNet.String())
|
||||
}
|
||||
if rulesetAttr.Scoped > 0 {
|
||||
elems = append(elems, "scoped: "+rulesetAttr.Scoped.String())
|
||||
}
|
||||
if len(elems) == 0 {
|
||||
return "0"
|
||||
}
|
||||
return strings.Join(elems, ", ")
|
||||
}
|
||||
|
||||
func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
|
||||
var pointer, size uintptr
|
||||
// NULL needed for abi version
|
||||
if rulesetAttr != nil {
|
||||
pointer = uintptr(unsafe.Pointer(rulesetAttr))
|
||||
size = unsafe.Sizeof(*rulesetAttr)
|
||||
}
|
||||
|
||||
rulesetFd, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags)
|
||||
fd = int(rulesetFd)
|
||||
err = errno
|
||||
|
||||
if fd < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if rulesetAttr != nil { // not a fd otherwise
|
||||
syscall.CloseOnExec(fd)
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
func LandlockGetABI() (int, error) {
|
||||
return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
|
||||
}
|
||||
|
||||
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
|
||||
r, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0)
|
||||
if r != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
61
container/landlock_test.go
Normal file
61
container/landlock_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
func TestLandlockString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
rulesetAttr *container.RulesetAttr
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, "NULL"},
|
||||
{"zero", new(container.RulesetAttr), "0"},
|
||||
{"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
|
||||
{"set", &container.RulesetAttr{
|
||||
HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
|
||||
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
|
||||
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
|
||||
{"all", &container.RulesetAttr{
|
||||
HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
|
||||
container.LANDLOCK_ACCESS_FS_WRITE_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_READ_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_READ_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_REG |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_SYM |
|
||||
container.LANDLOCK_ACCESS_FS_REFER |
|
||||
container.LANDLOCK_ACCESS_FS_TRUNCATE |
|
||||
container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
|
||||
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
|
||||
container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
|
||||
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
|
||||
container.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.rulesetAttr.String(); got != tc.want {
|
||||
t.Errorf("String: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLandlockAttrSize(t *testing.T) {
|
||||
want := 24
|
||||
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
|
||||
t.Errorf("Sizeof: %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
245
container/mount.go
Normal file
245
container/mount.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
. "syscall"
|
||||
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
/*
|
||||
Holding CAP_SYS_ADMIN within the user namespace that owns a process's mount namespace
|
||||
allows that process to create bind mounts and mount the following types of filesystems:
|
||||
- /proc (since Linux 3.8)
|
||||
- /sys (since Linux 3.8)
|
||||
- devpts (since Linux 3.9)
|
||||
- tmpfs(5) (since Linux 3.9)
|
||||
- ramfs (since Linux 3.9)
|
||||
- mqueue (since Linux 3.9)
|
||||
- bpf (since Linux 4.4)
|
||||
- overlayfs (since Linux 5.11)
|
||||
*/
|
||||
|
||||
const (
|
||||
// zeroString is a zero value string, it represents NULL when passed to mount.
|
||||
zeroString = ""
|
||||
|
||||
// SourceNone is used when the source value is ignored,
|
||||
// such as when remounting.
|
||||
SourceNone = "none"
|
||||
// SourceProc is used when mounting proc.
|
||||
// Note that any source value is allowed when fstype is [FstypeProc].
|
||||
SourceProc = "proc"
|
||||
// SourceDevpts is used when mounting devpts.
|
||||
// Note that any source value is allowed when fstype is [FstypeDevpts].
|
||||
SourceDevpts = "devpts"
|
||||
// SourceMqueue is used when mounting mqueue.
|
||||
// Note that any source value is allowed when fstype is [FstypeMqueue].
|
||||
SourceMqueue = "mqueue"
|
||||
// SourceOverlay is used when mounting overlay.
|
||||
// Note that any source value is allowed when fstype is [FstypeOverlay].
|
||||
SourceOverlay = "overlay"
|
||||
|
||||
// SourceTmpfsRootfs is used when mounting the tmpfs instance backing the intermediate root.
|
||||
SourceTmpfsRootfs = "rootfs"
|
||||
// SourceTmpfsDevtmpfs is used when mounting tmpfs representing a subset of host devtmpfs.
|
||||
SourceTmpfsDevtmpfs = "devtmpfs"
|
||||
// SourceTmpfsEphemeral is used when mounting a writable instance of tmpfs.
|
||||
SourceTmpfsEphemeral = "ephemeral"
|
||||
// SourceTmpfsReadonly is used when mounting a readonly instance of tmpfs.
|
||||
SourceTmpfsReadonly = "readonly"
|
||||
|
||||
// FstypeNULL is used when the fstype value is ignored,
|
||||
// such as when bind mounting or remounting.
|
||||
FstypeNULL = zeroString
|
||||
// FstypeProc represents the proc pseudo-filesystem.
|
||||
// A fully visible instance of proc must be available in the mount namespace for proc to be mounted.
|
||||
// This filesystem type is usually mounted on [FHSProc].
|
||||
FstypeProc = "proc"
|
||||
// FstypeDevpts represents the devpts pseudo-filesystem.
|
||||
// This type of filesystem is usually mounted on /dev/pts.
|
||||
FstypeDevpts = "devpts"
|
||||
// FstypeTmpfs represents the tmpfs filesystem.
|
||||
// This filesystem type can be mounted anywhere in the container filesystem.
|
||||
FstypeTmpfs = "tmpfs"
|
||||
// FstypeMqueue represents the mqueue pseudo-filesystem.
|
||||
// This filesystem type is usually mounted on /dev/mqueue.
|
||||
FstypeMqueue = "mqueue"
|
||||
// FstypeOverlay represents the overlay pseudo-filesystem.
|
||||
// This filesystem type can be mounted anywhere in the container filesystem.
|
||||
FstypeOverlay = "overlay"
|
||||
|
||||
// OptionOverlayLowerdir represents the lowerdir option of the overlay pseudo-filesystem.
|
||||
// Any filesystem, does not need to be on a writable filesystem.
|
||||
OptionOverlayLowerdir = "lowerdir"
|
||||
// OptionOverlayUpperdir represents the upperdir option of the overlay pseudo-filesystem.
|
||||
// The upperdir is normally on a writable filesystem.
|
||||
OptionOverlayUpperdir = "upperdir"
|
||||
// OptionOverlayWorkdir represents the workdir option of the overlay pseudo-filesystem.
|
||||
// The workdir needs to be an empty directory on the same filesystem as upperdir.
|
||||
OptionOverlayWorkdir = "workdir"
|
||||
// OptionOverlayUserxattr represents the userxattr option of the overlay pseudo-filesystem.
|
||||
// Use the "user.overlay." xattr namespace instead of "trusted.overlay.".
|
||||
OptionOverlayUserxattr = "userxattr"
|
||||
|
||||
// SpecialOverlayEscape is the escape string for overlay mount options.
|
||||
SpecialOverlayEscape = `\`
|
||||
// SpecialOverlayOption is the separator string between overlay mount options.
|
||||
SpecialOverlayOption = ","
|
||||
// SpecialOverlayPath is the separator string between overlay paths.
|
||||
SpecialOverlayPath = ":"
|
||||
)
|
||||
|
||||
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
|
||||
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
|
||||
|
||||
if eq {
|
||||
p.k.verbosef("resolved %q flags %#x", target, flags)
|
||||
} else {
|
||||
p.k.verbosef("resolved %q on %q flags %#x", source, target, flags)
|
||||
}
|
||||
|
||||
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot mount %q on %q:", source, target))
|
||||
}
|
||||
|
||||
return p.k.remount(target, flags)
|
||||
}
|
||||
|
||||
// remount applies flags on target, recursively if MS_REC is set.
|
||||
func (p *procPaths) remount(target string, flags uintptr) error {
|
||||
// syscallDispatcher methods bindMount, remount must not be called from this function
|
||||
|
||||
var targetFinal string
|
||||
if v, err := p.k.evalSymlinks(target); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
targetFinal = v
|
||||
if targetFinal != target {
|
||||
p.k.verbosef("target resolves to %q", targetFinal)
|
||||
}
|
||||
}
|
||||
|
||||
// final target path according to the kernel through proc
|
||||
var targetKFinal string
|
||||
{
|
||||
var destFd int
|
||||
if err := IgnoringEINTR(func() (err error) {
|
||||
destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
|
||||
return
|
||||
}); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot open %q:", targetFinal))
|
||||
}
|
||||
if v, err := p.k.readlink(p.fd(destFd)); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if err = p.k.close(destFd); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot close %q:", targetFinal))
|
||||
} else {
|
||||
targetKFinal = v
|
||||
}
|
||||
}
|
||||
|
||||
mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY
|
||||
return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
|
||||
n, err := d.Unfold(targetKFinal)
|
||||
if err != nil {
|
||||
if errors.Is(err, ESTALE) {
|
||||
return msg.WrapErr(err,
|
||||
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
|
||||
}
|
||||
return wrapErrSuffix(err,
|
||||
"cannot unfold mount hierarchy:")
|
||||
}
|
||||
|
||||
if err = remountWithFlags(p.k, n, mf); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot remount %q:", n.Clean))
|
||||
}
|
||||
if flags&MS_REC == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for cur := range n.Collective() {
|
||||
// avoid remounting twice
|
||||
if cur == n {
|
||||
continue
|
||||
}
|
||||
|
||||
err = remountWithFlags(p.k, cur, mf)
|
||||
|
||||
if err != nil && !errors.Is(err, EACCES) {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot propagate flags to %q:", cur.Clean))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
|
||||
func remountWithFlags(k syscallDispatcher, n *vfs.MountInfoNode, mf uintptr) error {
|
||||
// syscallDispatcher methods bindMount, remount must not be called from this function
|
||||
|
||||
kf, unmatched := n.Flags()
|
||||
if len(unmatched) != 0 {
|
||||
k.verbosef("unmatched vfs options: %q", unmatched)
|
||||
}
|
||||
|
||||
if kf&mf != mf {
|
||||
return k.mount(SourceNone, n.Clean, FstypeNULL, MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, zeroString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mountTmpfs mounts tmpfs on target;
|
||||
// callers who wish to mount to sysroot must pass the return value of toSysroot.
|
||||
func mountTmpfs(k syscallDispatcher, fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||
// syscallDispatcher.mountTmpfs must not be called from this function
|
||||
|
||||
if err := k.mkdirAll(target, parentPerm(perm)); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
opt := fmt.Sprintf("mode=%#o", perm)
|
||||
if size > 0 {
|
||||
opt += fmt.Sprintf(",size=%d", size)
|
||||
}
|
||||
return wrapErrSuffix(
|
||||
k.mount(fsname, target, FstypeTmpfs, flags, opt),
|
||||
fmt.Sprintf("cannot mount tmpfs on %q:", target))
|
||||
}
|
||||
|
||||
func parentPerm(perm os.FileMode) os.FileMode {
|
||||
pperm := 0755
|
||||
if perm&0070 == 0 {
|
||||
pperm &= ^0050
|
||||
}
|
||||
if perm&0007 == 0 {
|
||||
pperm &= ^0005
|
||||
}
|
||||
return os.FileMode(pperm)
|
||||
}
|
||||
|
||||
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
|
||||
func EscapeOverlayDataSegment(s string) string {
|
||||
if s == zeroString {
|
||||
return zeroString
|
||||
}
|
||||
|
||||
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
|
||||
s = f[0]
|
||||
}
|
||||
|
||||
return strings.NewReplacer(
|
||||
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
|
||||
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
|
||||
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
|
||||
).Replace(s)
|
||||
}
|
||||
305
container/mount_test.go
Normal file
305
container/mount_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
func TestBindMount(t *testing.T) {
|
||||
checkSimple(t, "bindMount", []simpleTestCase{
|
||||
{"mount", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
|
||||
}, [][]kexpect{{
|
||||
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
|
||||
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, errUnique},
|
||||
}}, wrapErrSuffix(errUnique, `cannot mount "/host/nix" on "/sysroot/nix":`)},
|
||||
|
||||
{"success ne", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false)
|
||||
}, [][]kexpect{{
|
||||
{"verbosef", expectArgs{"resolved %q on %q flags %#x", []any{"/host/nix", "/sysroot/.host-nix", uintptr(1)}}, nil, nil},
|
||||
{"mount", expectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil},
|
||||
{"remount", expectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil},
|
||||
}}, nil},
|
||||
|
||||
{"success", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
|
||||
}, [][]kexpect{{
|
||||
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
|
||||
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil},
|
||||
{"remount", expectArgs{"/sysroot/nix", uintptr(1)}, nil, nil},
|
||||
}}, nil},
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemount(t *testing.T) {
|
||||
const sampleMountinfoNix = `254 407 253:0 / /host rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
|
||||
255 254 0:28 / /host/mnt/.ro-cwd ro,noatime master:2 - 9p cwd ro,access=client,msize=16384,trans=virtio
|
||||
256 254 0:29 / /host/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio
|
||||
257 254 0:30 / /host/nix/store rw,relatime master:4 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work
|
||||
258 257 0:30 / /host/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work
|
||||
259 254 0:33 / /host/tmp/shared rw,relatime master:6 - 9p shared rw,access=client,msize=16384,trans=virtio
|
||||
260 254 0:34 / /host/tmp/xchg rw,relatime master:7 - 9p xchg rw,access=client,msize=16384,trans=virtio
|
||||
261 254 0:22 / /host/proc rw,nosuid,nodev,noexec,relatime master:8 - proc proc rw
|
||||
262 254 0:25 / /host/sys rw,nosuid,nodev,noexec,relatime master:9 - sysfs sysfs rw
|
||||
263 262 0:7 / /host/sys/kernel/security rw,nosuid,nodev,noexec,relatime master:10 - securityfs securityfs rw
|
||||
264 262 0:35 /../../.. /host/sys/fs/cgroup rw,nosuid,nodev,noexec,relatime master:11 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot
|
||||
265 262 0:36 / /host/sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:12 - pstore pstore rw
|
||||
266 262 0:37 / /host/sys/fs/bpf rw,nosuid,nodev,noexec,relatime master:13 - bpf bpf rw,mode=700
|
||||
267 262 0:12 / /host/sys/kernel/tracing rw,nosuid,nodev,noexec,relatime master:20 - tracefs tracefs rw
|
||||
268 262 0:8 / /host/sys/kernel/debug rw,nosuid,nodev,noexec,relatime master:21 - debugfs debugfs rw
|
||||
269 262 0:44 / /host/sys/kernel/config rw,nosuid,nodev,noexec,relatime master:64 - configfs configfs rw
|
||||
270 262 0:45 / /host/sys/fs/fuse/connections rw,nosuid,nodev,noexec,relatime master:66 - fusectl fusectl rw
|
||||
271 254 0:6 / /host/dev rw,nosuid master:14 - devtmpfs devtmpfs rw,size=200532k,nr_inodes=498943,mode=755
|
||||
324 271 0:20 / /host/dev/pts rw,nosuid,noexec,relatime master:15 - devpts devpts rw,gid=3,mode=620,ptmxmode=666
|
||||
378 271 0:21 / /host/dev/shm rw,nosuid,nodev master:16 - tmpfs tmpfs rw
|
||||
379 271 0:19 / /host/dev/mqueue rw,nosuid,nodev,noexec,relatime master:19 - mqueue mqueue rw
|
||||
388 271 0:38 / /host/dev/hugepages rw,nosuid,nodev,relatime master:22 - hugetlbfs hugetlbfs rw,pagesize=2M
|
||||
397 254 0:23 / /host/run rw,nosuid,nodev master:17 - tmpfs tmpfs rw,size=1002656k,mode=755
|
||||
398 397 0:24 / /host/run/keys rw,nosuid,nodev,relatime master:18 - ramfs ramfs rw,mode=750
|
||||
399 397 0:39 / /host/run/credentials/systemd-journald.service ro,nosuid,nodev,noexec,relatime,nosymfollow master:23 - tmpfs tmpfs rw,size=1024k,nr_inodes=1024,mode=700,noswap
|
||||
400 397 0:43 / /host/run/wrappers rw,nodev,relatime master:93 - tmpfs tmpfs rw,mode=755
|
||||
401 397 0:61 / /host/run/credentials/getty@tty1.service ro,nosuid,nodev,noexec,relatime,nosymfollow master:240 - tmpfs tmpfs rw,size=1024k,nr_inodes=1024,mode=700,noswap
|
||||
402 397 0:62 / /host/run/credentials/serial-getty@ttyS0.service ro,nosuid,nodev,noexec,relatime,nosymfollow master:288 - tmpfs tmpfs rw,size=1024k,nr_inodes=1024,mode=700,noswap
|
||||
403 397 0:63 / /host/run/user/1000 rw,nosuid,nodev,relatime master:295 - tmpfs tmpfs rw,size=401060k,nr_inodes=100265,mode=700,uid=1000,gid=100
|
||||
404 254 0:46 / /host/mnt/cwd rw,relatime master:96 - overlay overlay rw,lowerdir=/mnt/.ro-cwd,upperdir=/tmp/.cwd/upper,workdir=/tmp/.cwd/work
|
||||
405 254 0:47 / /host/mnt/src rw,relatime master:99 - overlay overlay rw,lowerdir=/nix/store/ihcrl3zwvp2002xyylri2wz0drwajx4z-ns0pa7q2b1jpx9pbf1l9352x6rniwxjn-source,upperdir=/tmp/.src/upper,workdir=/tmp/.src/work
|
||||
407 253 0:65 / / rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=1000000,gid=1000000
|
||||
408 407 0:65 /sysroot /sysroot rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=1000000,gid=1000000
|
||||
409 408 253:0 /bin /sysroot/bin rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
|
||||
410 408 253:0 /home /sysroot/home rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
|
||||
411 408 253:0 /lib64 /sysroot/lib64 rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
|
||||
412 408 253:0 /lost+found /sysroot/lost+found rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
|
||||
413 408 253:0 /nix /sysroot/nix rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
|
||||
414 413 0:29 / /sysroot/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio
|
||||
415 413 0:30 / /sysroot/nix/store rw,relatime master:4 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work
|
||||
416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work`
|
||||
|
||||
checkSimple(t, "remount", []simpleTestCase{
|
||||
{"evalSymlinks", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", errUnique},
|
||||
}}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"open", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, errUnique},
|
||||
}}, wrapErrSuffix(errUnique, `cannot open "/sysroot/nix":`)},
|
||||
|
||||
{"readlink", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", errUnique},
|
||||
}}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"close", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, errUnique},
|
||||
}}, wrapErrSuffix(errUnique, `cannot close "/sysroot/nix":`)},
|
||||
|
||||
{"mountinfo stale", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil},
|
||||
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil},
|
||||
{"open", expectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
||||
}}, msg.WrapErr(syscall.ESTALE, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)},
|
||||
|
||||
{"mountinfo", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil},
|
||||
}}, wrapErrSuffix(vfs.ErrMountInfoFields, `cannot parse mountinfo:`)},
|
||||
|
||||
{"mount", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, errUnique},
|
||||
}}, wrapErrSuffix(errUnique, `cannot remount "/sysroot/nix":`)},
|
||||
|
||||
{"mount propagate", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, errUnique},
|
||||
}}, wrapErrSuffix(errUnique, `cannot propagate flags to "/sysroot/nix/.ro-store":`)},
|
||||
|
||||
{"success toplevel", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/bin"}, "/sysroot/bin", nil},
|
||||
{"open", expectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil},
|
||||
{"close", expectArgs{0xbabe}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil},
|
||||
}}, nil},
|
||||
|
||||
{"success EACCES", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
|
||||
}}, nil},
|
||||
|
||||
{"success no propagate", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
||||
}}, nil},
|
||||
|
||||
{"success case sensitive", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
|
||||
}}, nil},
|
||||
|
||||
{"success", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||
}, [][]kexpect{{
|
||||
{"evalSymlinks", expectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil},
|
||||
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil},
|
||||
{"open", expectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
|
||||
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
|
||||
}}, nil},
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemountWithFlags(t *testing.T) {
|
||||
checkSimple(t, "remountWithFlags", []simpleTestCase{
|
||||
{"noop unmatched", func(k syscallDispatcher) error {
|
||||
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
|
||||
}, [][]kexpect{{
|
||||
{"verbosef", expectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil},
|
||||
}}, nil},
|
||||
|
||||
{"noop", func(k syscallDispatcher) error {
|
||||
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
|
||||
}, nil, nil},
|
||||
|
||||
{"success", func(k syscallDispatcher) error {
|
||||
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
|
||||
}, [][]kexpect{{
|
||||
{"mount", expectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil},
|
||||
}}, nil},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMountTmpfs(t *testing.T) {
|
||||
checkSimple(t, "mountTmpfs", []simpleTestCase{
|
||||
{"mkdirAll", func(k syscallDispatcher) error {
|
||||
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
|
||||
}, [][]kexpect{{
|
||||
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, errUnique},
|
||||
}}, wrapErrSelf(errUnique)},
|
||||
|
||||
{"success no size", func(k syscallDispatcher) error {
|
||||
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
|
||||
}, [][]kexpect{{
|
||||
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil},
|
||||
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil},
|
||||
}}, nil},
|
||||
|
||||
{"success", func(k syscallDispatcher) error {
|
||||
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
|
||||
}, [][]kexpect{{
|
||||
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil},
|
||||
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil},
|
||||
}}, nil},
|
||||
})
|
||||
}
|
||||
|
||||
func TestParentPerm(t *testing.T) {
|
||||
testCases := []struct {
|
||||
perm os.FileMode
|
||||
want os.FileMode
|
||||
}{
|
||||
{0755, 0755},
|
||||
{0750, 0750},
|
||||
{0705, 0705},
|
||||
{0700, 0700},
|
||||
{050, 0750},
|
||||
{05, 0705},
|
||||
{0, 0700},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.perm.String(), func(t *testing.T) {
|
||||
if got := parentPerm(tc.perm); got != tc.want {
|
||||
t.Errorf("parentPerm: %#o, want %#o", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeOverlayDataSegment(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
s string
|
||||
want string
|
||||
}{
|
||||
{"zero", zeroString, zeroString},
|
||||
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
|
||||
{"bwrap", `/path :,\`, `/path \:\,\\`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := EscapeOverlayDataSegment(tc.s); got != tc.want {
|
||||
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
68
container/msg.go
Normal file
68
container/msg.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Msg interface {
|
||||
IsVerbose() bool
|
||||
Verbose(v ...any)
|
||||
Verbosef(format string, v ...any)
|
||||
WrapErr(err error, a ...any) error
|
||||
PrintBaseErr(err error, fallback string)
|
||||
|
||||
Suspend()
|
||||
Resume() bool
|
||||
|
||||
BeforeExit()
|
||||
}
|
||||
|
||||
type DefaultMsg struct{ inactive atomic.Bool }
|
||||
|
||||
func (msg *DefaultMsg) IsVerbose() bool { return true }
|
||||
func (msg *DefaultMsg) Verbose(v ...any) {
|
||||
if !msg.inactive.Load() {
|
||||
log.Println(v...)
|
||||
}
|
||||
}
|
||||
func (msg *DefaultMsg) Verbosef(format string, v ...any) {
|
||||
if !msg.inactive.Load() {
|
||||
log.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// checkedWrappedErr implements error with strict checks for wrapped values.
|
||||
type checkedWrappedErr struct {
|
||||
err error
|
||||
a []any
|
||||
}
|
||||
|
||||
func (c *checkedWrappedErr) Error() string { return fmt.Sprintf("%v, a = %s", c.err, c.a) }
|
||||
func (c *checkedWrappedErr) Is(err error) bool {
|
||||
var concreteErr *checkedWrappedErr
|
||||
if !errors.As(err, &concreteErr) {
|
||||
return false
|
||||
}
|
||||
return reflect.DeepEqual(c, concreteErr)
|
||||
}
|
||||
|
||||
func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
|
||||
// provide a mostly bulletproof path to bypass this behaviour in tests
|
||||
if testing.Testing() && os.Getenv("GOPATH") != Nonexistent {
|
||||
return &checkedWrappedErr{err, a}
|
||||
}
|
||||
|
||||
log.Println(a...)
|
||||
return err
|
||||
}
|
||||
func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
|
||||
|
||||
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
|
||||
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
|
||||
func (msg *DefaultMsg) BeforeExit() {}
|
||||
162
container/msg_test.go
Normal file
162
container/msg_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
func TestDefaultMsg(t *testing.T) {
|
||||
// bypass WrapErr testing behaviour
|
||||
t.Setenv("GOPATH", container.Nonexistent)
|
||||
|
||||
{
|
||||
w := log.Writer()
|
||||
f := log.Flags()
|
||||
t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) })
|
||||
}
|
||||
msg := new(container.DefaultMsg)
|
||||
|
||||
t.Run("is verbose", func(t *testing.T) {
|
||||
if !msg.IsVerbose() {
|
||||
t.Error("IsVerbose unexpected outcome")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verbose", func(t *testing.T) {
|
||||
log.SetOutput(panicWriter{})
|
||||
msg.Suspend()
|
||||
msg.Verbose()
|
||||
msg.Verbosef("\x00")
|
||||
msg.Resume()
|
||||
|
||||
buf := new(strings.Builder)
|
||||
log.SetOutput(buf)
|
||||
log.SetFlags(0)
|
||||
msg.Verbose()
|
||||
msg.Verbosef("\x00")
|
||||
|
||||
want := "\n\x00\n"
|
||||
if buf.String() != want {
|
||||
t.Errorf("Verbose: %q, want %q", buf.String(), want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrapErr", func(t *testing.T) {
|
||||
buf := new(strings.Builder)
|
||||
log.SetOutput(buf)
|
||||
log.SetFlags(0)
|
||||
if err := msg.WrapErr(syscall.EBADE, "\x00", "\x00"); err != syscall.EBADE {
|
||||
t.Errorf("WrapErr: %v", err)
|
||||
}
|
||||
msg.PrintBaseErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:")
|
||||
|
||||
want := "\x00 \x00\ncannot cuddle cat: state not recoverable\n"
|
||||
if buf.String() != want {
|
||||
t.Errorf("WrapErr: %q, want %q", buf.String(), want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inactive", func(t *testing.T) {
|
||||
{
|
||||
inactive := msg.Resume()
|
||||
if inactive {
|
||||
t.Cleanup(func() { msg.Suspend() })
|
||||
}
|
||||
}
|
||||
|
||||
if msg.Resume() {
|
||||
t.Error("Resume unexpected outcome")
|
||||
}
|
||||
|
||||
msg.Suspend()
|
||||
if !msg.Resume() {
|
||||
t.Error("Resume unexpected outcome")
|
||||
}
|
||||
})
|
||||
|
||||
// the function is a noop
|
||||
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
|
||||
|
||||
t.Run("checkedWrappedErr", func(t *testing.T) {
|
||||
// temporarily re-enable testing behaviour
|
||||
t.Setenv("GOPATH", "")
|
||||
wrappedErr := msg.WrapErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:", syscall.ENOTRECOVERABLE)
|
||||
|
||||
t.Run("string", func(t *testing.T) {
|
||||
want := "state not recoverable, a = [cannot cuddle cat: state not recoverable]"
|
||||
if got := wrappedErr.Error(); got != want {
|
||||
t.Errorf("Error: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bad concrete type", func(t *testing.T) {
|
||||
if errors.Is(wrappedErr, syscall.ENOTRECOVERABLE) {
|
||||
t.Error("incorrect type assertion")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type panicWriter struct{}
|
||||
|
||||
func (panicWriter) Write([]byte) (int, error) { panic("unreachable") }
|
||||
|
||||
func saveRestoreOutput(t *testing.T) {
|
||||
out := container.GetOutput()
|
||||
t.Cleanup(func() { container.SetOutput(out) })
|
||||
}
|
||||
|
||||
func replaceOutput(t *testing.T) {
|
||||
saveRestoreOutput(t)
|
||||
container.SetOutput(&testOutput{t: t})
|
||||
}
|
||||
|
||||
type testOutput struct {
|
||||
t *testing.T
|
||||
suspended atomic.Bool
|
||||
}
|
||||
|
||||
func (out *testOutput) IsVerbose() bool { return testing.Verbose() }
|
||||
|
||||
func (out *testOutput) Verbose(v ...any) {
|
||||
if !out.IsVerbose() {
|
||||
return
|
||||
}
|
||||
out.t.Log(v...)
|
||||
}
|
||||
|
||||
func (out *testOutput) Verbosef(format string, v ...any) {
|
||||
if !out.IsVerbose() {
|
||||
return
|
||||
}
|
||||
out.t.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (out *testOutput) WrapErr(err error, a ...any) error { return hlog.WrapErr(err, a...) }
|
||||
func (out *testOutput) PrintBaseErr(err error, fallback string) { hlog.PrintBaseError(err, fallback) }
|
||||
|
||||
func (out *testOutput) Suspend() {
|
||||
if out.suspended.CompareAndSwap(false, true) {
|
||||
out.Verbose("suspend called")
|
||||
return
|
||||
}
|
||||
out.Verbose("suspend called on suspended output")
|
||||
}
|
||||
|
||||
func (out *testOutput) Resume() bool {
|
||||
if out.suspended.CompareAndSwap(true, false) {
|
||||
out.Verbose("resume called")
|
||||
return true
|
||||
}
|
||||
out.Verbose("resume called on unsuspended output")
|
||||
return false
|
||||
}
|
||||
|
||||
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }
|
||||
26
container/output.go
Normal file
26
container/output.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package container
|
||||
|
||||
var msg Msg = new(DefaultMsg)
|
||||
|
||||
func GetOutput() Msg { return msg }
|
||||
func SetOutput(v Msg) {
|
||||
if v == nil {
|
||||
msg = new(DefaultMsg)
|
||||
} else {
|
||||
msg = v
|
||||
}
|
||||
}
|
||||
|
||||
func wrapErrSuffix(err error, a ...any) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return msg.WrapErr(err, append(a, err)...)
|
||||
}
|
||||
|
||||
func wrapErrSelf(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return msg.WrapErr(err, err.Error())
|
||||
}
|
||||
110
container/output_test.go
Normal file
110
container/output_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetSetOutput(t *testing.T) {
|
||||
{
|
||||
out := GetOutput()
|
||||
t.Cleanup(func() { SetOutput(out) })
|
||||
}
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
SetOutput(new(stubOutput))
|
||||
if v, ok := GetOutput().(*DefaultMsg); ok {
|
||||
t.Fatalf("SetOutput: got unexpected output %#v", v)
|
||||
}
|
||||
SetOutput(nil)
|
||||
if _, ok := GetOutput().(*DefaultMsg); !ok {
|
||||
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stub", func(t *testing.T) {
|
||||
SetOutput(new(stubOutput))
|
||||
if _, ok := GetOutput().(*stubOutput); !ok {
|
||||
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrapErr(t *testing.T) {
|
||||
{
|
||||
out := GetOutput()
|
||||
t.Cleanup(func() { SetOutput(out) })
|
||||
}
|
||||
|
||||
var wrapFp *func(error, ...any) error
|
||||
s := new(stubOutput)
|
||||
SetOutput(s)
|
||||
wrapFp = &s.wrapF
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
f func(t *testing.T)
|
||||
wantErr error
|
||||
wantA []any
|
||||
}{
|
||||
{"suffix nil", func(t *testing.T) {
|
||||
if err := wrapErrSuffix(nil, "\x00"); err != nil {
|
||||
t.Errorf("wrapErrSuffix: %v", err)
|
||||
}
|
||||
}, nil, nil},
|
||||
{"suffix val", func(t *testing.T) {
|
||||
if err := wrapErrSuffix(syscall.ENOTRECOVERABLE, "\x00\x00"); err != syscall.ENOTRECOVERABLE {
|
||||
t.Errorf("wrapErrSuffix: %v", err)
|
||||
}
|
||||
}, syscall.ENOTRECOVERABLE, []any{"\x00\x00", syscall.ENOTRECOVERABLE}},
|
||||
{"self nil", func(t *testing.T) {
|
||||
if err := wrapErrSelf(nil); err != nil {
|
||||
t.Errorf("wrapErrSelf: %v", err)
|
||||
}
|
||||
}, nil, nil},
|
||||
{"self val", func(t *testing.T) {
|
||||
if err := wrapErrSelf(syscall.ENOTRECOVERABLE); err != syscall.ENOTRECOVERABLE {
|
||||
t.Errorf("wrapErrSelf: %v", err)
|
||||
}
|
||||
}, syscall.ENOTRECOVERABLE, []any{"state not recoverable"}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
gotErr error
|
||||
gotA []any
|
||||
)
|
||||
*wrapFp = func(err error, a ...any) error { gotErr = err; gotA = a; return err }
|
||||
|
||||
tc.f(t)
|
||||
if gotErr != tc.wantErr {
|
||||
t.Errorf("WrapErr: err = %v, want %v", gotErr, tc.wantErr)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotA, tc.wantA) {
|
||||
t.Errorf("WrapErr: a = %v, want %v", gotA, tc.wantA)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubOutput struct {
|
||||
wrapF func(error, ...any) error
|
||||
}
|
||||
|
||||
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
|
||||
func (*stubOutput) Verbose(...any) { panic("unreachable") }
|
||||
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
|
||||
func (*stubOutput) PrintBaseErr(error, string) { panic("unreachable") }
|
||||
func (*stubOutput) Suspend() { panic("unreachable") }
|
||||
func (*stubOutput) Resume() bool { panic("unreachable") }
|
||||
func (*stubOutput) BeforeExit() { panic("unreachable") }
|
||||
|
||||
func (s *stubOutput) WrapErr(err error, v ...any) error {
|
||||
if s.wrapF == nil {
|
||||
panic("unreachable")
|
||||
}
|
||||
return s.wrapF(err, v...)
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
package proc
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotSet = errors.New("environment variable not set")
|
||||
ErrInvalid = errors.New("bad file descriptor")
|
||||
ErrFdFormat = errors.New("bad file descriptor representation")
|
||||
)
|
||||
|
||||
// Setup appends the read end of a pipe for payload transmission and returns its fd.
|
||||
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
||||
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return -1, nil, err
|
||||
@@ -23,23 +24,25 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Receive retrieves payload pipe fd from the environment,
|
||||
// receives its payload and returns the Close method of the pipe.
|
||||
func Receive(key string, e any) (func() error, error) {
|
||||
// Receive retrieves setup fd from the environment and receives params.
|
||||
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
var setup *os.File
|
||||
|
||||
if s, ok := os.LookupEnv(key); !ok {
|
||||
return nil, ErrNotSet
|
||||
} else {
|
||||
if fd, err := strconv.Atoi(s); err != nil {
|
||||
return nil, err
|
||||
return nil, ErrFdFormat
|
||||
} else {
|
||||
setup = os.NewFile(uintptr(fd), "setup")
|
||||
if setup == nil {
|
||||
return nil, ErrInvalid
|
||||
return nil, syscall.EBADF
|
||||
}
|
||||
if fdp != nil {
|
||||
*fdp = setup.Fd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func() error { return setup.Close() }, gob.NewDecoder(setup).Decode(e)
|
||||
return setup.Close, gob.NewDecoder(setup).Decode(e)
|
||||
}
|
||||
120
container/params_test.go
Normal file
120
container/params_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
func TestSetupReceive(t *testing.T) {
|
||||
t.Run("not set", func(t *testing.T) {
|
||||
const key = "TEST_ENV_NOT_SET"
|
||||
{
|
||||
v, ok := os.LookupEnv(key)
|
||||
t.Cleanup(func() {
|
||||
if ok {
|
||||
if err := os.Setenv(key, v); err != nil {
|
||||
t.Fatalf("Setenv: error = %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatalf("Unsetenv: error = %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrNotSet) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, container.ErrNotSet)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format", func(t *testing.T) {
|
||||
const key = "TEST_ENV_FORMAT"
|
||||
t.Setenv(key, "")
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrFdFormat) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, container.ErrFdFormat)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("range", func(t *testing.T) {
|
||||
const key = "TEST_ENV_RANGE"
|
||||
t.Setenv(key, "-1")
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EBADF) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, syscall.EBADF)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("setup receive", func(t *testing.T) {
|
||||
check := func(t *testing.T, useNilFdp bool) {
|
||||
const key = "TEST_SETUP_RECEIVE"
|
||||
payload := []int{syscall.MS_MGC_VAL, syscall.MS_MGC_MSK, syscall.MS_ASYNC, syscall.MS_ACTIVE}
|
||||
|
||||
encoderDone := make(chan error, 1)
|
||||
extraFiles := make([]*os.File, 0, 1)
|
||||
if fd, encoder, err := container.Setup(&extraFiles); err != nil {
|
||||
t.Fatalf("Setup: error = %v", err)
|
||||
} else if fd != 3 {
|
||||
t.Fatalf("Setup: fd = %d, want 3", fd)
|
||||
} else {
|
||||
go func() { encoderDone <- encoder.Encode(payload) }()
|
||||
}
|
||||
|
||||
if len(extraFiles) != 1 {
|
||||
t.Fatalf("extraFiles: len = %v, want 1", len(extraFiles))
|
||||
}
|
||||
|
||||
var dupFd int
|
||||
if fd, err := syscall.Dup(int(extraFiles[0].Fd())); err != nil {
|
||||
t.Fatalf("Dup: error = %v", err)
|
||||
} else {
|
||||
syscall.CloseOnExec(fd)
|
||||
dupFd = fd
|
||||
t.Setenv(key, strconv.Itoa(fd))
|
||||
}
|
||||
|
||||
var (
|
||||
gotPayload []int
|
||||
fdp *uintptr
|
||||
)
|
||||
if !useNilFdp {
|
||||
fdp = new(uintptr)
|
||||
}
|
||||
var closeFile func() error
|
||||
if f, err := container.Receive(key, &gotPayload, fdp); err != nil {
|
||||
t.Fatalf("Receive: error = %v", err)
|
||||
} else {
|
||||
closeFile = f
|
||||
|
||||
if !slices.Equal(payload, gotPayload) {
|
||||
t.Errorf("Receive: %#v, want %#v", gotPayload, payload)
|
||||
}
|
||||
}
|
||||
if !useNilFdp {
|
||||
if int(*fdp) != dupFd {
|
||||
t.Errorf("Fd: %d, want %d", *fdp, dupFd)
|
||||
}
|
||||
}
|
||||
|
||||
if err := <-encoderDone; err != nil {
|
||||
t.Errorf("Encode: error = %v", err)
|
||||
}
|
||||
|
||||
if closeFile != nil {
|
||||
if err := closeFile(); err != nil {
|
||||
t.Errorf("Close: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("fp", func(t *testing.T) { check(t, false) })
|
||||
t.Run("nil", func(t *testing.T) { check(t, true) })
|
||||
})
|
||||
}
|
||||
162
container/path.go
Normal file
162
container/path.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
/* constants in this file bypass abs check, be extremely careful when changing them! */
|
||||
|
||||
const (
|
||||
// FHSRoot points to the file system root.
|
||||
FHSRoot = "/"
|
||||
// FHSEtc points to the directory for system-specific configuration.
|
||||
FHSEtc = "/etc/"
|
||||
// FHSTmp points to the place for small temporary files.
|
||||
FHSTmp = "/tmp/"
|
||||
|
||||
// FHSRun points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
|
||||
FHSRun = "/run/"
|
||||
// FHSRunUser points to a directory containing per-user runtime directories,
|
||||
// each usually individually mounted "tmpfs" instances.
|
||||
FHSRunUser = FHSRun + "user/"
|
||||
|
||||
// FHSUsr points to vendor-supplied operating system resources.
|
||||
FHSUsr = "/usr/"
|
||||
// FHSUsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
|
||||
FHSUsrBin = FHSUsr + "bin/"
|
||||
|
||||
// FHSVar points to persistent, variable system data. Writable during normal system operation.
|
||||
FHSVar = "/var/"
|
||||
// FHSVarLib points to persistent system data.
|
||||
FHSVarLib = FHSVar + "lib/"
|
||||
// FHSVarEmpty points to a nonstandard directory that is usually empty.
|
||||
FHSVarEmpty = FHSVar + "empty/"
|
||||
|
||||
// FHSDev points to the root directory for device nodes.
|
||||
FHSDev = "/dev/"
|
||||
// FHSProc points to a virtual kernel file system exposing the process list and other functionality.
|
||||
FHSProc = "/proc/"
|
||||
// FHSProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
|
||||
FHSProcSys = FHSProc + "sys/"
|
||||
// FHSSys points to a virtual kernel file system exposing discovered devices and other functionality.
|
||||
FHSSys = "/sys/"
|
||||
)
|
||||
|
||||
var (
|
||||
// AbsFHSRoot is [FHSRoot] as [Absolute].
|
||||
AbsFHSRoot = &Absolute{FHSRoot}
|
||||
// AbsFHSEtc is [FHSEtc] as [Absolute].
|
||||
AbsFHSEtc = &Absolute{FHSEtc}
|
||||
// AbsFHSTmp is [FHSTmp] as [Absolute].
|
||||
AbsFHSTmp = &Absolute{FHSTmp}
|
||||
|
||||
// AbsFHSRun is [FHSRun] as [Absolute].
|
||||
AbsFHSRun = &Absolute{FHSRun}
|
||||
// AbsFHSRunUser is [FHSRunUser] as [Absolute].
|
||||
AbsFHSRunUser = &Absolute{FHSRunUser}
|
||||
|
||||
// AbsFHSUsrBin is [FHSUsrBin] as [Absolute].
|
||||
AbsFHSUsrBin = &Absolute{FHSUsrBin}
|
||||
|
||||
// AbsFHSVar is [FHSVar] as [Absolute].
|
||||
AbsFHSVar = &Absolute{FHSVar}
|
||||
// AbsFHSVarLib is [FHSVarLib] as [Absolute].
|
||||
AbsFHSVarLib = &Absolute{FHSVarLib}
|
||||
|
||||
// AbsFHSDev is [FHSDev] as [Absolute].
|
||||
AbsFHSDev = &Absolute{FHSDev}
|
||||
// AbsFHSProc is [FHSProc] as [Absolute].
|
||||
AbsFHSProc = &Absolute{FHSProc}
|
||||
// AbsFHSSys is [FHSSys] as [Absolute].
|
||||
AbsFHSSys = &Absolute{FHSSys}
|
||||
)
|
||||
|
||||
const (
|
||||
// Nonexistent is a path that cannot exist.
|
||||
// /proc is chosen because a system with covered /proc is unsupported by this package.
|
||||
Nonexistent = FHSProc + "nonexistent"
|
||||
|
||||
hostPath = FHSRoot + hostDir
|
||||
hostDir = "host"
|
||||
sysrootPath = FHSRoot + sysrootDir
|
||||
sysrootDir = "sysroot"
|
||||
)
|
||||
|
||||
func toSysroot(name string) string {
|
||||
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
|
||||
return path.Join(sysrootPath, name)
|
||||
}
|
||||
|
||||
func toHost(name string) string {
|
||||
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
|
||||
return path.Join(hostPath, name)
|
||||
}
|
||||
|
||||
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
|
||||
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
|
||||
if err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
if content != nil {
|
||||
_, err = f.Write(content)
|
||||
}
|
||||
return errors.Join(f.Close(), wrapErrSelf(err))
|
||||
}
|
||||
|
||||
func ensureFile(name string, perm, pperm os.FileMode) error {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return createFile(name, perm, pperm, nil)
|
||||
}
|
||||
|
||||
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
|
||||
err = msg.WrapErr(syscall.EISDIR,
|
||||
fmt.Sprintf("path %q is a directory", name))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var hostProc = newProcPaths(direct{}, hostPath)
|
||||
|
||||
func newProcPaths(k syscallDispatcher, prefix string) *procPaths {
|
||||
return &procPaths{k, prefix + "/proc", prefix + "/proc/self"}
|
||||
}
|
||||
|
||||
type procPaths struct {
|
||||
k syscallDispatcher
|
||||
prefix string
|
||||
self string
|
||||
}
|
||||
|
||||
func (p *procPaths) stdout() string { return p.self + "/fd/1" }
|
||||
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
|
||||
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
|
||||
if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
d := vfs.NewMountInfoDecoder(r)
|
||||
err0 := f(d)
|
||||
if err = r.Close(); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if err = d.Err(); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot parse mountinfo:")
|
||||
}
|
||||
return err0
|
||||
}
|
||||
}
|
||||
252
container/path_test.go
Normal file
252
container/path_test.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
func TestToSysroot(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"", "/sysroot"},
|
||||
{"/", "/sysroot"},
|
||||
{"//etc///", "/sysroot/etc"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := toSysroot(tc.name); got != tc.want {
|
||||
t.Errorf("toSysroot: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToHost(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"", "/host"},
|
||||
{"/", "/host"},
|
||||
{"//etc///", "/host/etc"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := toHost(tc.name); got != tc.want {
|
||||
t.Errorf("toHost: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// InternalToHostOvlEscape exports toHost passed to EscapeOverlayDataSegment.
|
||||
func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(toHost(s)) }
|
||||
|
||||
func TestCreateFile(t *testing.T) {
|
||||
t.Run("nonexistent", func(t *testing.T) {
|
||||
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: "/proc/nonexistent",
|
||||
Err: syscall.ENOENT,
|
||||
})) {
|
||||
t.Errorf("createFile: error = %v", err)
|
||||
}
|
||||
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
|
||||
Op: "open",
|
||||
Path: "/proc/nonexistent",
|
||||
Err: syscall.ENOENT,
|
||||
})) {
|
||||
t.Errorf("createFile: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("touch", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
pathname := path.Join(tempDir, "empty")
|
||||
if err := createFile(pathname, 0644, 0755, nil); err != nil {
|
||||
t.Fatalf("createFile: error = %v", err)
|
||||
}
|
||||
if d, err := os.ReadFile(pathname); err != nil {
|
||||
t.Fatalf("ReadFile: error = %v", err)
|
||||
} else if len(d) != 0 {
|
||||
t.Fatalf("createFile: %q", string(d))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
pathname := path.Join(tempDir, "zero")
|
||||
if err := createFile(pathname, 0644, 0755, []byte{0}); err != nil {
|
||||
t.Fatalf("createFile: error = %v", err)
|
||||
}
|
||||
if d, err := os.ReadFile(pathname); err != nil {
|
||||
t.Fatalf("ReadFile: error = %v", err)
|
||||
} else if string(d) != "\x00" {
|
||||
t.Fatalf("createFile: %q, want %q", string(d), "\x00")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnsureFile(t *testing.T) {
|
||||
t.Run("create", func(t *testing.T) {
|
||||
if err := ensureFile(path.Join(t.TempDir(), "ensure"), 0644, 0755); err != nil {
|
||||
t.Errorf("ensureFile: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stat", func(t *testing.T) {
|
||||
t.Run("inaccessible", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
pathname := path.Join(tempDir, "inaccessible")
|
||||
if f, err := os.Create(pathname); err != nil {
|
||||
t.Fatalf("Create: error = %v", err)
|
||||
} else {
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
if err := os.Chmod(tempDir, 0); err != nil {
|
||||
t.Fatalf("Chmod: error = %v", err)
|
||||
}
|
||||
|
||||
wantErr := wrapErrSelf(&os.PathError{
|
||||
Op: "stat",
|
||||
Path: pathname,
|
||||
Err: syscall.EACCES,
|
||||
})
|
||||
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
|
||||
t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
|
||||
if err := os.Chmod(tempDir, 0755); err != nil {
|
||||
t.Fatalf("Chmod: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory", func(t *testing.T) {
|
||||
pathname := t.TempDir()
|
||||
wantErr := msg.WrapErr(syscall.EISDIR, fmt.Sprintf("path %q is a directory", pathname))
|
||||
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
|
||||
t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ensure", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
pathname := path.Join(tempDir, "ensure")
|
||||
if f, err := os.Create(pathname); err != nil {
|
||||
t.Fatalf("Create: error = %v", err)
|
||||
} else {
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
if err := ensureFile(pathname, 0644, 0755); err != nil {
|
||||
t.Errorf("ensureFile: error = %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcPaths(t *testing.T) {
|
||||
t.Run("host", func(t *testing.T) {
|
||||
t.Run("stdout", func(t *testing.T) {
|
||||
want := "/host/proc/self/fd/1"
|
||||
if got := hostProc.stdout(); got != want {
|
||||
t.Errorf("stdout: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("fd", func(t *testing.T) {
|
||||
want := "/host/proc/self/fd/9223372036854775807"
|
||||
if got := hostProc.fd(math.MaxInt64); got != want {
|
||||
t.Errorf("stdout: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("mountinfo", func(t *testing.T) {
|
||||
t.Run("nonexistent", func(t *testing.T) {
|
||||
nonexistentProc := newProcPaths(direct{}, t.TempDir())
|
||||
wantErr := wrapErrSelf(&os.PathError{
|
||||
Op: "open",
|
||||
Path: nonexistentProc.self + "/mountinfo",
|
||||
Err: syscall.ENOENT,
|
||||
})
|
||||
if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !errors.Is(err, wantErr) {
|
||||
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sample", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
if err := os.MkdirAll(path.Join(tempDir, "proc/self"), 0755); err != nil {
|
||||
t.Fatalf("MkdirAll: error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("clean", func(t *testing.T) {
|
||||
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte(`15 20 0:3 / /proc rw,relatime - proc /proc rw
|
||||
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
|
||||
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755`), 0644); err != nil {
|
||||
t.Fatalf("WriteFile: error = %v", err)
|
||||
}
|
||||
|
||||
var mountInfo *vfs.MountInfo
|
||||
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(&mountInfo) }); err != nil {
|
||||
t.Fatalf("mountinfo: error = %v", err)
|
||||
}
|
||||
|
||||
wantMountInfo := &vfs.MountInfo{Next: &vfs.MountInfo{Next: &vfs.MountInfo{
|
||||
MountInfoEntry: vfs.MountInfoEntry{ID: 17, Parent: 20, Devno: vfs.DevT{0, 5}, Root: "/", Target: "/dev", VfsOptstr: "rw,relatime", OptFields: []string{}, FsType: "devtmpfs", Source: "udev", FsOptstr: "rw,size=1983516k,nr_inodes=495879,mode=755"}},
|
||||
MountInfoEntry: vfs.MountInfoEntry{ID: 16, Parent: 20, Devno: vfs.DevT{0, 15}, Root: "/", Target: "/sys", VfsOptstr: "rw,relatime", OptFields: []string{}, FsType: "sysfs", Source: "/sys", FsOptstr: "rw"}},
|
||||
MountInfoEntry: vfs.MountInfoEntry{ID: 15, Parent: 20, Devno: vfs.DevT{0, 3}, Root: "/", Target: "/proc", VfsOptstr: "rw,relatime", OptFields: []string{}, FsType: "proc", Source: "/proc", FsOptstr: "rw"},
|
||||
}
|
||||
if !reflect.DeepEqual(mountInfo, wantMountInfo) {
|
||||
t.Errorf("Decode: %#v, want %#v", mountInfo, wantMountInfo)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("closed", func(t *testing.T) {
|
||||
p := newProcPaths(direct{}, tempDir)
|
||||
wantErr := wrapErrSelf(&os.PathError{
|
||||
Op: "close",
|
||||
Path: p.self + "/mountinfo",
|
||||
Err: os.ErrClosed,
|
||||
})
|
||||
if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error {
|
||||
v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r")
|
||||
v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr()))
|
||||
if f, ok := v.Elem().Interface().(io.ReadCloser); !ok {
|
||||
t.Fatal("implementation of bufio.Scanner no longer compatible with this fault injection")
|
||||
return syscall.ENOTRECOVERABLE
|
||||
} else {
|
||||
return f.Close()
|
||||
}
|
||||
}); !errors.Is(err, wantErr) {
|
||||
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed", func(t *testing.T) {
|
||||
path.Join(tempDir, "proc/self/mountinfo")
|
||||
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte{0}, 0644); err != nil {
|
||||
t.Fatalf("WriteFile: error = %v", err)
|
||||
}
|
||||
|
||||
wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:")
|
||||
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("mountinfo: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
24
container/seccomp/hash_amd64_test.go
Normal file
24
container/seccomp/hash_amd64_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package seccomp_test
|
||||
|
||||
import . "hakurei.app/container/seccomp"
|
||||
|
||||
var bpfExpected = bpfLookup{
|
||||
{AllowMultiarch | AllowCAN |
|
||||
AllowBluetooth, PresetExt |
|
||||
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
|
||||
PresetLinux32}: toHash(
|
||||
"e99dd345e195413473d3cbee07b4ed57b908bfa89ea2072fe93482847f50b5b758da17e74ca2bbc00813de49a2b9bf834c024ed48850be69b68a9a4c5f53a9db"),
|
||||
|
||||
{0, 0}: toHash(
|
||||
"95ec69d017733e072160e0da80fdebecdf27ae8166f5e2a731270c98ea2d2946cb5231029063668af215879155da21aca79b070e04c0ee9acdf58f55cfa815a5"),
|
||||
{0, PresetExt}: toHash(
|
||||
"dc7f2e1c5e829b79ebb7efc759150f54a83a75c8df6fee4dce5dadc4736c585d4deebfeb3c7969af3a077e90b77bb4741db05d90997c8659b95891206ac9952d"),
|
||||
{0, PresetStrict}: toHash(
|
||||
"e880298df2bd6751d0040fc21bc0ed4c00f95dc0d7ba506c244d8b8cf6866dba8ef4a33296f287b66cccc1d78e97026597f84cc7dec1573e148960fbd35cd735"),
|
||||
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
|
||||
"39871b93ffafc8b979fcedc0b0c37b9e03922f5b02748dc5c3c17c92527f6e022ede1f48bff59246ea452c0d1de54827808b1a6f84f32bbde1aa02ae30eedcfa"),
|
||||
{0, PresetExt | PresetDenyDevel}: toHash(
|
||||
"c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a"),
|
||||
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
|
||||
"0b76007476c1c9e25dbf674c29fdf609a1656a70063e49327654e1b5360ad3da06e1a3e32bf80e961c5516ad83d4b9e7e9bde876a93797e27627d2555c25858b"),
|
||||
}
|
||||
24
container/seccomp/hash_arm64_test.go
Normal file
24
container/seccomp/hash_arm64_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package seccomp_test
|
||||
|
||||
import . "hakurei.app/container/seccomp"
|
||||
|
||||
var bpfExpected = bpfLookup{
|
||||
{AllowMultiarch | AllowCAN |
|
||||
AllowBluetooth, PresetExt |
|
||||
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
|
||||
PresetLinux32}: toHash(
|
||||
"1431c013f2ddac3adae577821cb5d351b1514e7c754d62346ddffd31f46ea02fb368e46e3f8104f81019617e721fe687ddd83f1e79580622ccc991da12622170"),
|
||||
|
||||
{0, 0}: toHash(
|
||||
"450c21210dbf124dfa7ae56d0130f9c2e24b26f5bce8795ee75766c75850438ff9e7d91c5e73d63bbe51a5d4b06c2a0791c4de2903b2b9805f16265318183235"),
|
||||
{0, PresetExt}: toHash(
|
||||
"d971d0f2d30f54ac920fc6d84df2be279e9fd28cf2d48be775d7fdbd790b750e1369401cd3bb8bcf9ba3adb91874fe9792d9e3f62209b8ee59c9fdd2ddd10c7b"),
|
||||
{0, PresetStrict}: toHash(
|
||||
"79318538a3dc851314b6bd96f10d5861acb2aa7e13cb8de0619d0f6a76709d67f01ef3fd67e195862b02f9711e5b769bc4d1eb4fc0dfc41a723c89c968a93297"),
|
||||
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
|
||||
"228286c2f5df8e44463be0a57b91977b7f38b63b09e5d98dfabe5c61545b8f9ac3e5ea3d86df55d7edf2ce61875f0a5a85c0ab82800bef178c42533e8bdc9a6c"),
|
||||
{0, PresetExt | PresetDenyDevel}: toHash(
|
||||
"433ce9b911282d6dcc8029319fb79b816b60d5a795ec8fc94344dd027614d68f023166a91bb881faaeeedd26e3d89474e141e5a69a97e93b8984ca8f14999980"),
|
||||
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
|
||||
"cf1f4dc87436ba8ec95d268b663a6397bb0b4a5ac64d8557e6cc529d8b0f6f65dad3a92b62ed29d85eee9c6dde1267757a4d0f86032e8a45ca1bceadfa34cf5e"),
|
||||
}
|
||||
28
container/seccomp/hash_test.go
Normal file
28
container/seccomp/hash_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package seccomp_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
type (
|
||||
bpfPreset = struct {
|
||||
seccomp.ExportFlag
|
||||
seccomp.FilterPreset
|
||||
}
|
||||
bpfLookup map[bpfPreset][]byte
|
||||
)
|
||||
|
||||
func toHash(s string) []byte {
|
||||
if len(s) != 128 {
|
||||
panic("bad sha512 string length")
|
||||
}
|
||||
if v, err := hex.DecodeString(s); err != nil {
|
||||
panic(err.Error())
|
||||
} else if len(v) != 64 {
|
||||
panic("unreachable")
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
130
container/seccomp/libseccomp-helper.c
Normal file
130
container/seccomp/libseccomp-helper.c
Normal file
@@ -0,0 +1,130 @@
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE /* CLONE_NEWUSER */
|
||||
#endif
|
||||
|
||||
#include "libseccomp-helper.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
||||
|
||||
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
|
||||
uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags) {
|
||||
int i;
|
||||
int last_allowed_family;
|
||||
int disallowed;
|
||||
struct hakurei_syscall_rule *rule;
|
||||
|
||||
int32_t res = 0; /* refer to resPrefix for message */
|
||||
|
||||
/* Blocklist all but unix, inet, inet6 and netlink */
|
||||
struct {
|
||||
int family;
|
||||
hakurei_export_flag flags_mask;
|
||||
} socket_family_allowlist[] = {
|
||||
/* NOTE: Keep in numerical order */
|
||||
{AF_UNSPEC, 0},
|
||||
{AF_LOCAL, 0},
|
||||
{AF_INET, 0},
|
||||
{AF_INET6, 0},
|
||||
{AF_NETLINK, 0},
|
||||
{AF_CAN, HAKUREI_EXPORT_CAN},
|
||||
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
|
||||
};
|
||||
|
||||
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
|
||||
if (ctx == NULL) {
|
||||
res = 1;
|
||||
goto out;
|
||||
} else
|
||||
errno = 0;
|
||||
|
||||
/* We only really need to handle arches on multiarch systems.
|
||||
* If only one arch is supported the default is fine */
|
||||
if (arch != 0) {
|
||||
/* This *adds* the target arch, instead of replacing the
|
||||
* native one. This is not ideal, because we'd like to only
|
||||
* allow the target arch, but we can't really disallow the
|
||||
* native arch at this point, because then bubblewrap
|
||||
* couldn't continue running. */
|
||||
*ret_p = seccomp_arch_add(ctx, arch);
|
||||
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||
res = 2;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
|
||||
*ret_p = seccomp_arch_add(ctx, multiarch);
|
||||
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||
res = 3;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < rules_sz; i++) {
|
||||
rule = &rules[i];
|
||||
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
|
||||
|
||||
if (rule->arg)
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
||||
rule->syscall, 1, *rule->arg);
|
||||
else
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
||||
rule->syscall, 0);
|
||||
|
||||
if (*ret_p == -EFAULT) {
|
||||
res = 4;
|
||||
goto out;
|
||||
} else if (*ret_p < 0) {
|
||||
res = 5;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Socket filtering doesn't work on e.g. i386, so ignore failures here
|
||||
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
|
||||
* something else: https://github.com/seccomp/libseccomp/issues/8 */
|
||||
last_allowed_family = -1;
|
||||
for (i = 0; i < LEN(socket_family_allowlist); i++) {
|
||||
if (socket_family_allowlist[i].flags_mask != 0 &&
|
||||
(socket_family_allowlist[i].flags_mask & flags) !=
|
||||
socket_family_allowlist[i].flags_mask)
|
||||
continue;
|
||||
|
||||
for (disallowed = last_allowed_family + 1;
|
||||
disallowed < socket_family_allowlist[i].family; disallowed++) {
|
||||
/* Blocklist the in-between valid families */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
|
||||
SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_EQ, disallowed));
|
||||
}
|
||||
last_allowed_family = socket_family_allowlist[i].family;
|
||||
}
|
||||
/* Blocklist the rest */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
|
||||
|
||||
if (fd < 0) {
|
||||
*ret_p = seccomp_load(ctx);
|
||||
if (*ret_p != 0) {
|
||||
res = 7;
|
||||
goto out;
|
||||
}
|
||||
} else {
|
||||
*ret_p = seccomp_export_bpf(ctx, fd);
|
||||
if (*ret_p != 0) {
|
||||
res = 6;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
if (ctx)
|
||||
seccomp_release(ctx);
|
||||
|
||||
return res;
|
||||
}
|
||||
24
container/seccomp/libseccomp-helper.h
Normal file
24
container/seccomp/libseccomp-helper.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <seccomp.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
|
||||
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
|
||||
#error This package requires libseccomp >= v2.5.1
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
|
||||
HAKUREI_EXPORT_CAN = 1 << 1,
|
||||
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
|
||||
} hakurei_export_flag;
|
||||
|
||||
struct hakurei_syscall_rule {
|
||||
int syscall;
|
||||
int m_errno;
|
||||
struct scmp_arg_cmp *arg;
|
||||
};
|
||||
|
||||
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
|
||||
uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user