Compare commits
314 Commits
2a4e2724a3
...
v0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
ad1bc6794f
|
|||
|
e55822c62f
|
|||
|
802e6afa34
|
|||
|
e906cae9ee
|
|||
|
ae2df2c450
|
|||
|
6e3f34f2ec
|
|||
|
65a0bb9729
|
|||
|
afa7a0800d
|
|||
|
773253fdf5
|
|||
|
409ed172c8
|
|||
|
1c4f593566
|
|||
|
b99c63337d
|
|||
|
f09133a224
|
|||
|
16409b37a2
|
|||
|
a2a291791c
|
|||
|
8690419c2d
|
|||
|
1cdc6b4246
|
|||
|
56aad8dc11
|
|||
|
83c4f8b767
|
|||
|
d0ddd71934
|
|||
|
70e02090f7
|
|||
|
ca247b8037
|
|||
|
3f25c3f0af
|
|||
|
e271fa77aa
|
|||
|
f876043844
|
|||
|
6265aea73a
|
|||
|
c8a0effe90
|
|||
|
8df01b71d4
|
|||
|
985c4dd2fc
|
|||
|
da2b9c01ce
|
|||
|
323d132c40
|
|||
|
6cc2b406a4
|
|||
|
fcd0f2ede7
|
|||
|
e68db7fbfc
|
|||
|
ac81cfbedc
|
|||
|
05db06c87b
|
|||
|
e603b688ca
|
|||
|
a9def08533
|
|||
|
ecaf43358d
|
|||
|
197fa65b8f
|
|||
|
e81a45e849
|
|||
|
3920acf8c2
|
|||
|
19630a9593
|
|||
|
4051577d6b
|
|||
|
ddfb865e2d
|
|||
|
024d2ff782
|
|||
|
6f719bc3c1
|
|||
|
1b5d20a39b
|
|||
|
49600a6f46
|
|||
|
b489a3bba1
|
|||
|
780e3e5465
|
|||
|
712cfc06d7
|
|||
|
f5abce9df5
|
|||
|
ddb003e39b
|
|||
|
b12c290f12
|
|||
|
0122593312
|
|||
|
6aa431d57a
|
|||
|
08eeafe817
|
|||
|
d7c7c69a13
|
|||
|
50972096cd
|
|||
|
905b9f9785
|
|||
|
1c7e634f09
|
|||
|
8d472ebf2b
|
|||
|
4da6463135
|
|||
|
eb3385d490
|
|||
|
b8669338da
|
|||
|
f24dd4ab8c
|
|||
|
a462341a0a
|
|||
|
84ad9791e2
|
|||
|
b14690aa77
|
|||
|
d0b6852cd7
|
|||
|
da0459aca1
|
|||
|
1be8de6f5c
|
|||
|
0f41d96671
|
|||
|
92f510a647
|
|||
|
acb6931f3e
|
|||
|
9d932d1039
|
|||
|
9bc8532d56
|
|||
|
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
|
@@ -20,5 +20,5 @@ jobs:
|
|||||||
uses: https://gitea.com/actions/release-action@main
|
uses: https://gitea.com/actions/release-action@main
|
||||||
with:
|
with:
|
||||||
files: |-
|
files: |-
|
||||||
result/fortify-**
|
result/hakurei-**
|
||||||
api_key: '${{secrets.RELEASE_TOKEN}}'
|
api_key: '${{secrets.RELEASE_TOKEN}}'
|
||||||
|
|||||||
@@ -5,42 +5,25 @@ on:
|
|||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fortify:
|
hakurei:
|
||||||
name: Fortify
|
name: Hakurei
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run NixOS test
|
- name: Run NixOS test
|
||||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.fortify
|
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hakurei
|
||||||
|
|
||||||
- name: Upload test output
|
- name: Upload test output
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: "fortify-vm-output"
|
name: "hakurei-vm-output"
|
||||||
path: result/*
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
fpkg:
|
|
||||||
name: Fpkg
|
|
||||||
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.fpkg
|
|
||||||
|
|
||||||
- name: Upload test output
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: "fpkg-vm-output"
|
|
||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
race:
|
race:
|
||||||
name: Data race detector
|
name: Hakurei (race detector)
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -52,16 +35,69 @@ jobs:
|
|||||||
- name: Upload test output
|
- name: Upload test output
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: "fortify-race-vm-output"
|
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/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
check:
|
check:
|
||||||
name: Flake checks
|
name: Flake checks
|
||||||
needs:
|
needs:
|
||||||
- fortify
|
- hakurei
|
||||||
- fpkg
|
|
||||||
- race
|
- race
|
||||||
|
- sandbox
|
||||||
|
- sandbox-race
|
||||||
|
- hpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -80,15 +116,15 @@ jobs:
|
|||||||
- name: Build for test
|
- name: Build for test
|
||||||
id: build-test
|
id: build-test
|
||||||
run: >-
|
run: >-
|
||||||
export FORTIFY_REV="$(git rev-parse --short HEAD)" &&
|
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
|
||||||
sed -i.old 's/version = /version = "0.0.0-'$FORTIFY_REV'"; # version = /' package.nix &&
|
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
|
||||||
nix build --print-out-paths --print-build-logs .#dist &&
|
nix build --print-out-paths --print-build-logs .#dist &&
|
||||||
mv package.nix.old package.nix &&
|
mv package.nix.old package.nix &&
|
||||||
echo "rev=$FORTIFY_REV" >> $GITHUB_OUTPUT
|
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload test build
|
- name: Upload test build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: "fortify-${{ steps.build-test.outputs.rev }}"
|
name: "hakurei-${{ steps.build-test.outputs.rev }}"
|
||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
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
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
*.pkg
|
*.pkg
|
||||||
/fortify
|
/hakurei
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
@@ -26,7 +26,10 @@ go.work.sum
|
|||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# go generate
|
# go generate
|
||||||
security-context-v1-protocol.*
|
/cmd/hakurei/LICENSE
|
||||||
|
|
||||||
# release
|
# 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:
|
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)
|
<p align="center">
|
||||||
[](https://goreportcard.com/report/git.gensokyo.uk/security/fortify)
|
<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
|
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
|
||||||
module to configure target users and provide launchers and desktop files for your privileged user.
|
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.
|
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
|
||||||
|
|
||||||
- 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).
|
|
||||||
|
|
||||||
To use the module, import it into your configuration with
|
To use the module, import it into your configuration with
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||||
|
|
||||||
fortify = {
|
hakurei = {
|
||||||
url = "git+https://git.gensokyo.uk/security/fortify";
|
url = "git+https://git.gensokyo.uk/security/hakurei";
|
||||||
|
|
||||||
# Optional but recommended to limit the size of your system closure.
|
# Optional but recommended to limit the size of your system closure.
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
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";
|
system = "x86_64-linux";
|
||||||
modules = [
|
modules = [
|
||||||
fortify.nixosModules.fortify
|
hakurei.nixosModules.hakurei
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This adds the `environment.fortify` option:
|
This adds the `environment.hakurei` option:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
environment.fortify = {
|
environment.hakurei = {
|
||||||
enable = true;
|
enable = true;
|
||||||
stateDir = "/var/lib/persist/module/fortify";
|
stateDir = "/var/lib/hakurei";
|
||||||
users = {
|
users = {
|
||||||
alice = 0;
|
alice = 0;
|
||||||
nixos = 10;
|
nixos = 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
apps = [
|
commonPaths = [
|
||||||
{
|
{
|
||||||
|
src = "/sdcard";
|
||||||
|
write = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
extraHomeConfig = {
|
||||||
|
home.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
|
||||||
|
apps = {
|
||||||
|
"org.chromium.Chromium" = {
|
||||||
name = "chromium";
|
name = "chromium";
|
||||||
id = "org.chromium.Chromium";
|
identity = 1;
|
||||||
packages = [ pkgs.chromium ];
|
packages = [ pkgs.chromium ];
|
||||||
userns = true;
|
userns = true;
|
||||||
mapRealUid = true;
|
mapRealUid = true;
|
||||||
@@ -104,16 +111,20 @@ This adds the `environment.fortify` option:
|
|||||||
broadcast = { };
|
broadcast = { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
{
|
|
||||||
|
"org.claws_mail.Claws-Mail" = {
|
||||||
name = "claws-mail";
|
name = "claws-mail";
|
||||||
id = "org.claws_mail.Claws-Mail";
|
identity = 2;
|
||||||
packages = [ pkgs.claws-mail ];
|
packages = [ pkgs.claws-mail ];
|
||||||
gpu = false;
|
gpu = false;
|
||||||
capability.pulse = false;
|
capability.pulse = false;
|
||||||
}
|
};
|
||||||
{
|
|
||||||
|
"org.weechat" = {
|
||||||
name = "weechat";
|
name = "weechat";
|
||||||
|
identity = 3;
|
||||||
|
shareUid = true;
|
||||||
packages = [ pkgs.weechat ];
|
packages = [ pkgs.weechat ];
|
||||||
capability = {
|
capability = {
|
||||||
wayland = false;
|
wayland = false;
|
||||||
@@ -121,10 +132,12 @@ This adds the `environment.fortify` option:
|
|||||||
dbus = true;
|
dbus = true;
|
||||||
pulse = false;
|
pulse = false;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
{
|
|
||||||
|
"dev.vencord.Vesktop" = {
|
||||||
name = "discord";
|
name = "discord";
|
||||||
id = "dev.vencord.Vesktop";
|
identity = 3;
|
||||||
|
shareUid = true;
|
||||||
packages = [ pkgs.vesktop ];
|
packages = [ pkgs.vesktop ];
|
||||||
share = pkgs.vesktop;
|
share = pkgs.vesktop;
|
||||||
command = "vesktop --ozone-platform-hint=wayland";
|
command = "vesktop --ozone-platform-hint=wayland";
|
||||||
@@ -142,9 +155,12 @@ This adds the `environment.fortify` option:
|
|||||||
};
|
};
|
||||||
system.filter = true;
|
system.filter = true;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
{
|
|
||||||
|
"io.looking-glass" = {
|
||||||
name = "looking-glass-client";
|
name = "looking-glass-client";
|
||||||
|
identity = 4;
|
||||||
|
useCommonPaths = false;
|
||||||
groups = [ "plugdev" ];
|
groups = [ "plugdev" ];
|
||||||
extraPaths = [
|
extraPaths = [
|
||||||
{
|
{
|
||||||
@@ -155,8 +171,8 @@ This adds the `environment.fortify` option:
|
|||||||
extraConfig = {
|
extraConfig = {
|
||||||
programs.looking-glass-client.enable = true;
|
programs.looking-glass-client.enable = true;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
];
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
#include "acl-update.h"
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <sys/acl.h>
|
|
||||||
#include <acl/libacl.h>
|
|
||||||
|
|
||||||
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen) {
|
|
||||||
int ret = -1;
|
|
||||||
bool v;
|
|
||||||
int i;
|
|
||||||
acl_t acl;
|
|
||||||
acl_entry_t entry;
|
|
||||||
acl_tag_t tag_type;
|
|
||||||
void *qualifier_p;
|
|
||||||
acl_permset_t permset;
|
|
||||||
|
|
||||||
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
|
||||||
if (acl == NULL)
|
|
||||||
goto out;
|
|
||||||
|
|
||||||
// prune entries by uid
|
|
||||||
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
|
||||||
if (acl_get_tag_type(entry, &tag_type) != 0)
|
|
||||||
return -1;
|
|
||||||
if (tag_type != ACL_USER)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
qualifier_p = acl_get_qualifier(entry);
|
|
||||||
if (qualifier_p == NULL)
|
|
||||||
return -1;
|
|
||||||
v = *(uid_t *)qualifier_p == uid;
|
|
||||||
acl_free(qualifier_p);
|
|
||||||
|
|
||||||
if (!v)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
acl_delete_entry(acl, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plen == 0)
|
|
||||||
goto set;
|
|
||||||
|
|
||||||
if (acl_create_entry(&acl, &entry) != 0)
|
|
||||||
goto out;
|
|
||||||
if (acl_get_permset(entry, &permset) != 0)
|
|
||||||
goto out;
|
|
||||||
for (i = 0; i < plen; i++) {
|
|
||||||
if (acl_add_perm(permset, perms[i]) != 0)
|
|
||||||
goto out;
|
|
||||||
}
|
|
||||||
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
|
||||||
goto out;
|
|
||||||
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
|
||||||
goto out;
|
|
||||||
|
|
||||||
set:
|
|
||||||
if (acl_calc_mask(&acl) != 0)
|
|
||||||
goto out;
|
|
||||||
if (acl_valid(acl) != 0)
|
|
||||||
goto out;
|
|
||||||
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
|
||||||
ret = 0;
|
|
||||||
|
|
||||||
out:
|
|
||||||
free((void *)path_p);
|
|
||||||
if (acl != NULL)
|
|
||||||
acl_free((void *)acl);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#include <sys/acl.h>
|
|
||||||
|
|
||||||
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen);
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
package acl_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
getFAclInvocation struct {
|
|
||||||
cmd *exec.Cmd
|
|
||||||
val []*getFAclResp
|
|
||||||
pe []error
|
|
||||||
}
|
|
||||||
|
|
||||||
getFAclResp struct {
|
|
||||||
typ fAclType
|
|
||||||
cred int32
|
|
||||||
val fAclPerm
|
|
||||||
|
|
||||||
raw []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
fAclPerm uintptr
|
|
||||||
fAclType uint8
|
|
||||||
)
|
|
||||||
|
|
||||||
const fAclBufSize = 16
|
|
||||||
|
|
||||||
const (
|
|
||||||
fAclPermRead fAclPerm = 1 << iota
|
|
||||||
fAclPermWrite
|
|
||||||
fAclPermExecute
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fAclTypeUser fAclType = iota
|
|
||||||
fAclTypeGroup
|
|
||||||
fAclTypeMask
|
|
||||||
fAclTypeOther
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *getFAclInvocation) run(name string) error {
|
|
||||||
if c.cmd != nil {
|
|
||||||
panic("attempted to run twice")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
|
|
||||||
|
|
||||||
scanErr := make(chan error, 1)
|
|
||||||
if p, err := c.cmd.StdoutPipe(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
go c.parse(p, scanErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.cmd.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Join(<-scanErr, c.cmd.Wait())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
|
|
||||||
c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
|
|
||||||
|
|
||||||
s := bufio.NewScanner(pipe)
|
|
||||||
for s.Scan() {
|
|
||||||
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
|
|
||||||
if len(fields) != 3 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := getFAclResp{}
|
|
||||||
|
|
||||||
switch string(fields[0]) {
|
|
||||||
case "user":
|
|
||||||
resp.typ = fAclTypeUser
|
|
||||||
case "group":
|
|
||||||
resp.typ = fAclTypeGroup
|
|
||||||
case "mask":
|
|
||||||
resp.typ = fAclTypeMask
|
|
||||||
case "other":
|
|
||||||
resp.typ = fAclTypeOther
|
|
||||||
default:
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fields[1]) == 0 {
|
|
||||||
resp.cred = -1
|
|
||||||
} else {
|
|
||||||
if cred, err := strconv.Atoi(string(fields[1])); err != nil {
|
|
||||||
c.pe = append(c.pe, err)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
resp.cred = int32(cred)
|
|
||||||
if resp.cred < 0 {
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fields[2]) != 3 {
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
switch fields[2][0] {
|
|
||||||
case 'r':
|
|
||||||
resp.val |= fAclPermRead
|
|
||||||
case '-':
|
|
||||||
default:
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch fields[2][1] {
|
|
||||||
case 'w':
|
|
||||||
resp.val |= fAclPermWrite
|
|
||||||
case '-':
|
|
||||||
default:
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch fields[2][2] {
|
|
||||||
case 'x':
|
|
||||||
resp.val |= fAclPermExecute
|
|
||||||
case '-':
|
|
||||||
default:
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.raw = make([]byte, len(s.Bytes()))
|
|
||||||
copy(resp.raw, s.Bytes())
|
|
||||||
c.val = append(c.val, &resp)
|
|
||||||
}
|
|
||||||
scanErr <- s.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *getFAclResp) String() string {
|
|
||||||
if r.raw != nil && len(r.raw) > 0 {
|
|
||||||
return string(r.raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "(user-initialised resp value)"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
|
|
||||||
return r.typ == typ && r.cred == cred && r.val == val
|
|
||||||
}
|
|
||||||
125
acl/acl_test.go
125
acl/acl_test.go
@@ -1,125 +0,0 @@
|
|||||||
package acl_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
|
||||||
)
|
|
||||||
|
|
||||||
const testFileName = "acl.test"
|
|
||||||
|
|
||||||
var (
|
|
||||||
uid = os.Geteuid()
|
|
||||||
cred = int32(os.Geteuid())
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUpdatePerm(t *testing.T) {
|
|
||||||
if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
|
|
||||||
t.Log("acl test skipped")
|
|
||||||
t.SkipNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
testFilePath := path.Join(t.TempDir(), testFileName)
|
|
||||||
|
|
||||||
if f, err := os.Create(testFilePath); err != nil {
|
|
||||||
t.Fatalf("Create: error = %v", err)
|
|
||||||
} else {
|
|
||||||
if err = f.Close(); err != nil {
|
|
||||||
t.Fatalf("Close: error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := os.Remove(testFilePath); err != nil {
|
|
||||||
t.Fatalf("Remove: error = %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
cur := getfacl(t, testFilePath)
|
|
||||||
|
|
||||||
t.Run("default entry count", func(t *testing.T) {
|
|
||||||
if len(cur) != 3 {
|
|
||||||
t.Fatalf("unexpected test file acl length %d", len(cur))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("default clear mask", func(t *testing.T) {
|
|
||||||
if err := acl.Update(testFilePath, uid); err != nil {
|
|
||||||
t.Fatalf("UpdatePerm: error = %v", err)
|
|
||||||
}
|
|
||||||
if cur = getfacl(t, testFilePath); len(cur) != 4 {
|
|
||||||
t.Fatalf("UpdatePerm: %v", cur)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("default clear consistency", func(t *testing.T) {
|
|
||||||
if err := acl.Update(testFilePath, uid); err != nil {
|
|
||||||
t.Fatalf("UpdatePerm: error = %v", err)
|
|
||||||
}
|
|
||||||
if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
|
|
||||||
t.Fatalf("UpdatePerm: %v, want %v", val, cur)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read)
|
|
||||||
testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write)
|
|
||||||
testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute)
|
|
||||||
testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute)
|
|
||||||
testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute)
|
|
||||||
testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write)
|
|
||||||
testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if err := acl.Update(testFilePath, uid); err != nil {
|
|
||||||
t.Fatalf("UpdatePerm: error = %v", err)
|
|
||||||
}
|
|
||||||
if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
|
|
||||||
t.Fatalf("UpdatePerm: %v, want %v", v, cur)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := acl.Update(testFilePath, uid, perms...); err != nil {
|
|
||||||
t.Fatalf("UpdatePerm: error = %v", err)
|
|
||||||
}
|
|
||||||
r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
|
|
||||||
if r == nil {
|
|
||||||
t.Fatalf("UpdatePerm did not add an ACL entry")
|
|
||||||
}
|
|
||||||
if !r.equals(fAclTypeUser, cred, val) {
|
|
||||||
t.Fatalf("UpdatePerm(%s) = %s", name, r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getfacl(t *testing.T, name string) []*getFAclResp {
|
|
||||||
c := new(getFAclInvocation)
|
|
||||||
if err := c.run(name); err != nil {
|
|
||||||
t.Fatalf("getfacl: error = %v", err)
|
|
||||||
}
|
|
||||||
if len(c.pe) != 0 {
|
|
||||||
t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...).Error())
|
|
||||||
}
|
|
||||||
return c.val
|
|
||||||
}
|
|
||||||
|
|
||||||
func respByCred(v []*getFAclResp, typ fAclType, cred int32) *getFAclResp {
|
|
||||||
j := -1
|
|
||||||
for i, r := range v {
|
|
||||||
if r.typ == typ && r.cred == cred {
|
|
||||||
if j != -1 {
|
|
||||||
panic("invalid acl")
|
|
||||||
}
|
|
||||||
j = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if j == -1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return v[j]
|
|
||||||
}
|
|
||||||
149
cmd/fpkg/app.go
149
cmd/fpkg/app.go
@@ -1,149 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
type appInfo 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]
|
|
||||||
Devel bool `json:"devel,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]
|
|
||||||
Tty bool `json:"tty,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.Enablement `json:"enablements"`
|
|
||||||
|
|
||||||
// passed through to [fst.Config]
|
|
||||||
Multiarch bool `json:"multiarch,omitempty"`
|
|
||||||
// passed through to [fst.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 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 (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
|
|
||||||
config := &fst.Config{
|
|
||||||
ID: app.ID,
|
|
||||||
Path: argv[0],
|
|
||||||
Args: argv,
|
|
||||||
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),
|
|
||||||
Devel: app.Devel,
|
|
||||||
Userns: app.Userns,
|
|
||||||
Net: app.Net,
|
|
||||||
Dev: app.Dev,
|
|
||||||
Tty: app.Tty || flagDropShell,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if app.Multiarch {
|
|
||||||
config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
|
|
||||||
}
|
|
||||||
if app.Bluetooth {
|
|
||||||
config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
return bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatHostname(name string) string {
|
|
||||||
if h, err := os.Hostname(); err != nil {
|
|
||||||
log.Printf("cannot get hostname: %v", err)
|
|
||||||
return "fortify-" + name
|
|
||||||
} else {
|
|
||||||
return h + "-" + name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"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 {
|
|
||||||
log.Fatalf("%s: command not found", file)
|
|
||||||
return ""
|
|
||||||
} else {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var beforeRunFail = new(atomic.Pointer[func()])
|
|
||||||
|
|
||||||
func mustRun(name string, arg ...string) {
|
|
||||||
fmsg.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 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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},
|
|
||||||
}...)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/app"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
|
|
||||||
rs := new(fst.RunState)
|
|
||||||
a := app.MustNew(ctx, std)
|
|
||||||
|
|
||||||
if sa, err := a.Seal(config); err != nil {
|
|
||||||
fmsg.PrintBaseError(err, "cannot seal app:")
|
|
||||||
rs.ExitCode = 1
|
|
||||||
} else {
|
|
||||||
// this updates ExitCode
|
|
||||||
app.PrintRunStateErr(rs, sa.Run(rs))
|
|
||||||
}
|
|
||||||
|
|
||||||
if rs.ExitCode != 0 {
|
|
||||||
beforeFail()
|
|
||||||
os.Exit(rs.ExitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
108
cmd/fpkg/with.go
108
cmd/fpkg/with.go
@@ -1,108 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
|
||||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func withNixDaemon(
|
|
||||||
ctx context.Context,
|
|
||||||
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
|
|
||||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
|
||||||
) {
|
|
||||||
mustRunAppDropShell(ctx, updateConfig(&fst.Config{
|
|
||||||
ID: app.ID,
|
|
||||||
Path: shellPath,
|
|
||||||
Args: []string{shellPath, "-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,
|
|
||||||
Seccomp: seccomp.FlagMultiarch,
|
|
||||||
Tty: 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(
|
|
||||||
ctx context.Context,
|
|
||||||
action string, command []string, workDir string,
|
|
||||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
|
||||||
mustRunAppDropShell(ctx, &fst.Config{
|
|
||||||
ID: app.ID,
|
|
||||||
Path: shellPath,
|
|
||||||
Args: []string{shellPath, "-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,
|
|
||||||
Seccomp: seccomp.FlagMultiarch,
|
|
||||||
Tty: 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 mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
|
|
||||||
if dropShell {
|
|
||||||
config.Args = []string{shellPath, "-l"}
|
|
||||||
mustRunApp(ctx, config, beforeFail)
|
|
||||||
beforeFail()
|
|
||||||
internal.Exit(0)
|
|
||||||
}
|
|
||||||
mustRunApp(ctx, config, beforeFail)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
buildGoModule,
|
|
||||||
fortify ? abort "fortify package required",
|
|
||||||
}:
|
|
||||||
|
|
||||||
buildGoModule {
|
|
||||||
pname = "${fortify.pname}-fsu";
|
|
||||||
inherit (fortify) version;
|
|
||||||
|
|
||||||
src = ./.;
|
|
||||||
inherit (fortify) vendorHash;
|
|
||||||
CGO_ENABLED = 0;
|
|
||||||
|
|
||||||
preBuild = ''
|
|
||||||
go mod init fsu >& /dev/null
|
|
||||||
'';
|
|
||||||
|
|
||||||
ldflags =
|
|
||||||
lib.attrsets.foldlAttrs
|
|
||||||
(
|
|
||||||
ldflags: name: value:
|
|
||||||
ldflags ++ [ "-X main.${name}=${value}" ]
|
|
||||||
)
|
|
||||||
[ "-s -w" ]
|
|
||||||
{
|
|
||||||
fmain = "${fortify}/libexec/fortify";
|
|
||||||
fpkg = "${fortify}/libexec/fpkg";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
249
cmd/hakurei/command.go
Normal file
249
cmd/hakurei/command.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"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(ctx context.Context, 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:]...)
|
||||||
|
|
||||||
|
app.Main(ctx, config)
|
||||||
|
panic("unreachable")
|
||||||
|
})
|
||||||
|
|
||||||
|
{
|
||||||
|
var (
|
||||||
|
flagDBusConfigSession string
|
||||||
|
flagDBusConfigSystem string
|
||||||
|
flagDBusMpris bool
|
||||||
|
flagDBusVerbose bool
|
||||||
|
|
||||||
|
flagID string
|
||||||
|
flagIdentity int
|
||||||
|
flagGroups command.RepeatableFlag
|
||||||
|
flagHomeDir string
|
||||||
|
flagUserName string
|
||||||
|
|
||||||
|
flagWayland, flagX11, flagDBus, flagPulse bool
|
||||||
|
)
|
||||||
|
|
||||||
|
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||||
|
// initialise config from flags
|
||||||
|
config := &hst.Config{
|
||||||
|
ID: flagID,
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagIdentity < 0 || flagIdentity > 9999 {
|
||||||
|
log.Fatalf("identity %d out of range", flagIdentity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve home/username from os when flag is unset
|
||||||
|
var (
|
||||||
|
passwd *user.User
|
||||||
|
passwdOnce sync.Once
|
||||||
|
passwdFunc = func() {
|
||||||
|
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity))
|
||||||
|
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 flagHomeDir == "os" {
|
||||||
|
passwdOnce.Do(passwdFunc)
|
||||||
|
flagHomeDir = passwd.HomeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagUserName == "chronos" {
|
||||||
|
passwdOnce.Do(passwdFunc)
|
||||||
|
flagUserName = passwd.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Identity = flagIdentity
|
||||||
|
config.Groups = flagGroups
|
||||||
|
config.Username = flagUserName
|
||||||
|
|
||||||
|
if a, err := container.NewAbs(flagHomeDir); err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
config.Home = a
|
||||||
|
}
|
||||||
|
|
||||||
|
var e system.Enablement
|
||||||
|
if flagWayland {
|
||||||
|
e |= system.EWayland
|
||||||
|
}
|
||||||
|
if flagX11 {
|
||||||
|
e |= system.EX11
|
||||||
|
}
|
||||||
|
if flagDBus {
|
||||||
|
e |= system.EDBus
|
||||||
|
}
|
||||||
|
if flagPulse {
|
||||||
|
e |= system.EPulse
|
||||||
|
}
|
||||||
|
config.Enablements = hst.NewEnablements(e)
|
||||||
|
|
||||||
|
// parse D-Bus config file from flags if applicable
|
||||||
|
if flagDBus {
|
||||||
|
if flagDBusConfigSession == "builtin" {
|
||||||
|
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
|
||||||
|
} else {
|
||||||
|
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil {
|
||||||
|
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
|
||||||
|
} else {
|
||||||
|
config.SessionBus = conf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// system bus proxy is optional
|
||||||
|
if flagDBusConfigSystem != "nil" {
|
||||||
|
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != nil {
|
||||||
|
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
|
||||||
|
} else {
|
||||||
|
config.SystemBus = conf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// override log from configuration
|
||||||
|
if flagDBusVerbose {
|
||||||
|
if config.SessionBus != nil {
|
||||||
|
config.SessionBus.Log = true
|
||||||
|
}
|
||||||
|
if config.SystemBus != nil {
|
||||||
|
config.SystemBus.Log = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Main(ctx, config)
|
||||||
|
panic("unreachable")
|
||||||
|
}).
|
||||||
|
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||||
|
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
||||||
|
Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||||
|
"Path to system bus proxy config file, or \"nil\" to disable").
|
||||||
|
Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
|
||||||
|
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
||||||
|
Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
|
||||||
|
"Force buffered logging in the D-Bus proxy").
|
||||||
|
Flag(&flagID, "id", command.StringFlag(""),
|
||||||
|
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
||||||
|
Flag(&flagIdentity, "a", command.IntFlag(0),
|
||||||
|
"Application identity").
|
||||||
|
Flag(nil, "g", &flagGroups,
|
||||||
|
"Groups inherited by all container processes").
|
||||||
|
Flag(&flagHomeDir, "d", command.StringFlag("os"),
|
||||||
|
"Container home directory").
|
||||||
|
Flag(&flagUserName, "u", command.StringFlag("chronos"),
|
||||||
|
"Passwd user name within sandbox").
|
||||||
|
Flag(&flagWayland, "wayland", command.BoolFlag(false),
|
||||||
|
"Enable connection to Wayland via security-context-v1").
|
||||||
|
Flag(&flagX11, "X", command.BoolFlag(false),
|
||||||
|
"Enable direct connection to X11").
|
||||||
|
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
||||||
|
"Enable proxied connection to D-Bus").
|
||||||
|
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
||||||
|
"Enable direct connection to PulseAudio")
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var flagShort bool
|
||||||
|
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||||
|
switch len(args) {
|
||||||
|
case 0: // system
|
||||||
|
printShowSystem(os.Stdout, flagShort, 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, flagShort, flagJSON)
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Fatal("show requires 1 argument")
|
||||||
|
}
|
||||||
|
return errSuccess
|
||||||
|
}).Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var flagShort bool
|
||||||
|
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||||
|
var sc hst.Paths
|
||||||
|
app.CopyPaths(&sc, new(app.Hsu).MustID())
|
||||||
|
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON)
|
||||||
|
return errSuccess
|
||||||
|
}).Flag(&flagShort, "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
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/command"
|
"hakurei.app/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelp(t *testing.T) {
|
func TestHelp(t *testing.T) {
|
||||||
@@ -17,14 +17,14 @@ func TestHelp(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"main", []string{}, `
|
"main", []string{}, `
|
||||||
Usage: fortify [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
app Launch app defined by the specified config file
|
app Load app from configuration file
|
||||||
run Configure and start a permissive default sandbox
|
run Configure and start a permissive default sandbox
|
||||||
show Show the contents of an app configuration
|
show Show live or local app configuration
|
||||||
ps List active apps and their state
|
ps List active instances
|
||||||
version Show fortify version
|
version Display version information
|
||||||
license Show full license text
|
license Show full license text
|
||||||
template Produce a config template
|
template Produce a config template
|
||||||
help Show this help message
|
help Show this help message
|
||||||
@@ -33,34 +33,34 @@ Commands:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"run", []string{"run", "-h"}, `
|
"run", []string{"run", "-h"}, `
|
||||||
Usage: fortify 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]
|
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:
|
Flags:
|
||||||
-X Share X11 socket and allow connection
|
-X Enable direct connection to X11
|
||||||
-a int
|
-a int
|
||||||
Fortify application ID
|
Application identity
|
||||||
-d string
|
-d string
|
||||||
Application home directory (default "os")
|
Container home directory (default "os")
|
||||||
-dbus
|
-dbus
|
||||||
Proxy D-Bus connection
|
Enable proxied connection to D-Bus
|
||||||
-dbus-config string
|
-dbus-config string
|
||||||
Path to D-Bus proxy config file, or "builtin" for defaults (default "builtin")
|
Path to session bus proxy config file, or "builtin" for defaults (default "builtin")
|
||||||
-dbus-log
|
-dbus-log
|
||||||
Force logging in the D-Bus proxy
|
Force buffered logging in the D-Bus proxy
|
||||||
-dbus-system string
|
-dbus-system string
|
||||||
Path to system D-Bus proxy config file, or "nil" to disable (default "nil")
|
Path to system bus proxy config file, or "nil" to disable (default "nil")
|
||||||
-g value
|
-g value
|
||||||
Groups inherited by the app process
|
Groups inherited by all container processes
|
||||||
-id string
|
-id string
|
||||||
App ID, leave empty to disable security context app_id
|
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||||
-mpris
|
-mpris
|
||||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||||
-pulse
|
-pulse
|
||||||
Share PulseAudio socket and cookie
|
Enable direct connection to PulseAudio
|
||||||
-u string
|
-u string
|
||||||
Passwd name within sandbox (default "chronos")
|
Passwd user name within sandbox (default "chronos")
|
||||||
-wayland
|
-wayland
|
||||||
Allow Wayland connections
|
Enable connection to Wayland via security-context-v1
|
||||||
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
@@ -68,7 +68,7 @@ Flags:
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
out := new(bytes.Buffer)
|
out := new(bytes.Buffer)
|
||||||
c := buildCommand(out)
|
c := buildCommand(t.Context(), out)
|
||||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||||
t.Errorf("Parse: error = %v; want %v",
|
t.Errorf("Parse: error = %v; want %v",
|
||||||
err, command.ErrHelp)
|
err, command.ErrHelp)
|
||||||
60
cmd/hakurei/main.go
Normal file
60
cmd/hakurei/main.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// this works around go:embed '..' limitation
|
||||||
|
//go:generate cp ../../LICENSE .
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/internal"
|
||||||
|
"hakurei.app/internal/hlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errSuccess = errors.New("success")
|
||||||
|
|
||||||
|
//go:embed LICENSE
|
||||||
|
license string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { hlog.Prepare("hakurei") }
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(),
|
||||||
|
syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop() // unreachable
|
||||||
|
|
||||||
|
buildCommand(ctx, 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")
|
||||||
|
}
|
||||||
@@ -10,19 +10,20 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"hakurei.app/hst"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"hakurei.app/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/state"
|
"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
|
var r io.Reader
|
||||||
config = new(fst.Config)
|
config = new(hst.Config)
|
||||||
|
|
||||||
if name != "-" {
|
if name != "-" {
|
||||||
r = tryFd(name)
|
r = tryFd(name)
|
||||||
if r == nil {
|
if r == nil {
|
||||||
fmsg.Verbose("load configuration from file")
|
hlog.Verbose("load configuration from file")
|
||||||
|
|
||||||
if f, err := os.Open(name); err != nil {
|
if f, err := os.Open(name); err != nil {
|
||||||
log.Fatalf("cannot access configuration file %q: %s", name, err)
|
log.Fatalf("cannot access configuration file %q: %s", name, err)
|
||||||
@@ -51,11 +52,11 @@ func tryPath(name string) (config *fst.Config) {
|
|||||||
func tryFd(name string) io.ReadCloser {
|
func tryFd(name string) io.ReadCloser {
|
||||||
if v, err := strconv.Atoi(name); err != nil {
|
if v, err := strconv.Atoi(name); err != nil {
|
||||||
if !errors.Is(err, strconv.ErrSyntax) {
|
if !errors.Is(err, strconv.ErrSyntax) {
|
||||||
fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
|
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
fmsg.Verbosef("trying config stream from %d", v)
|
hlog.Verbosef("trying config stream from %d", v)
|
||||||
fd := uintptr(v)
|
fd := uintptr(v)
|
||||||
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
|
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
|
||||||
if errors.Is(errno, syscall.EBADF) {
|
if errors.Is(errno, syscall.EBADF) {
|
||||||
@@ -67,7 +68,7 @@ func tryFd(name string) io.ReadCloser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryShort(name string) (config *fst.Config, instance *state.State) {
|
func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||||
likePrefix := false
|
likePrefix := false
|
||||||
if len(name) <= 32 {
|
if len(name) <= 32 {
|
||||||
likePrefix = true
|
likePrefix = true
|
||||||
@@ -85,9 +86,11 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
|
|||||||
|
|
||||||
// try to match from state store
|
// try to match from state store
|
||||||
if likePrefix && len(name) >= 8 {
|
if likePrefix && len(name) >= 8 {
|
||||||
fmsg.Verbose("argument looks like prefix")
|
hlog.Verbose("argument looks like prefix")
|
||||||
|
|
||||||
s := state.NewMulti(std.Paths().RunDirPath)
|
var sc hst.Paths
|
||||||
|
app.CopyPaths(&sc, new(app.Hsu).MustID())
|
||||||
|
s := state.NewMulti(sc.RunDirPath.String())
|
||||||
if entries, err := state.Join(s); err != nil {
|
if entries, err := state.Join(s); err != nil {
|
||||||
log.Printf("cannot join store: %v", err)
|
log.Printf("cannot join store: %v", err)
|
||||||
// drop to fetch from file
|
// drop to fetch from file
|
||||||
@@ -96,12 +99,12 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
|
|||||||
v := id.String()
|
v := id.String()
|
||||||
if strings.HasPrefix(v, name) {
|
if strings.HasPrefix(v, name) {
|
||||||
// match, use config from this state entry
|
// match, use config from this state entry
|
||||||
instance = entries[id]
|
entry = entries[id]
|
||||||
config = instance.Config
|
config = entry.Config
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
fmsg.Verbosef("instance %s skipped", v)
|
hlog.Verbosef("instance %s skipped", v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,32 +5,24 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"hakurei.app/hst"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"hakurei.app/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"hakurei.app/internal/app/state"
|
||||||
"git.gensokyo.uk/security/fortify/internal/state"
|
"hakurei.app/system/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
|
||||||
info := new(fst.Info)
|
info := &hst.Info{User: new(app.Hsu).MustID()}
|
||||||
|
app.CopyPaths(&info.Paths, info.User)
|
||||||
// get fid by querying uid of aid 0
|
|
||||||
if uid, err := std.Uid(0); err != nil {
|
|
||||||
fmsg.PrintBaseError(err, "cannot obtain uid from fsu:")
|
|
||||||
os.Exit(1)
|
|
||||||
} else {
|
|
||||||
info.User = (uid / 10000) - 100
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
printJSON(output, short, info)
|
printJSON(output, short, info)
|
||||||
@@ -38,11 +30,15 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Printf("User:\t%d\n", info.User)
|
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(
|
func printShowInstance(
|
||||||
output io.Writer, now time.Time,
|
output io.Writer, now time.Time,
|
||||||
instance *state.State, config *fst.Config,
|
instance *state.State, config *hst.Config,
|
||||||
short, flagJSON bool) {
|
short, flagJSON bool) {
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
if instance != nil {
|
if instance != nil {
|
||||||
@@ -56,7 +52,7 @@ func printShowInstance(
|
|||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
|
||||||
if config.Confinement.Sandbox == nil {
|
if config.Container == nil {
|
||||||
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
|
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,19 +65,21 @@ func printShowInstance(
|
|||||||
|
|
||||||
t.Printf("App\n")
|
t.Printf("App\n")
|
||||||
if config.ID != "" {
|
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 {
|
} 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())
|
t.Printf(" Enablements:\t%s\n", config.Enablements.Unwrap().String())
|
||||||
if len(config.Confinement.Groups) > 0 {
|
if len(config.Groups) > 0 {
|
||||||
t.Printf(" Groups:\t%q\n", config.Confinement.Groups)
|
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
||||||
}
|
}
|
||||||
t.Printf(" Directory:\t%s\n", config.Confinement.Outer)
|
if config.Home != nil {
|
||||||
if config.Confinement.Sandbox != nil {
|
t.Printf(" Home:\t%s\n", config.Home)
|
||||||
sandbox := config.Confinement.Sandbox
|
}
|
||||||
if sandbox.Hostname != "" {
|
if config.Container != nil {
|
||||||
t.Printf(" Hostname:\t%q\n", sandbox.Hostname)
|
params := config.Container
|
||||||
|
if params.Hostname != "" {
|
||||||
|
t.Printf(" Hostname:\t%s\n", params.Hostname)
|
||||||
}
|
}
|
||||||
flags := make([]string, 0, 7)
|
flags := make([]string, 0, 7)
|
||||||
writeFlag := func(name string, value bool) {
|
writeFlag := func(name string, value bool) {
|
||||||
@@ -89,68 +87,43 @@ func printShowInstance(
|
|||||||
flags = append(flags, name)
|
flags = append(flags, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeFlag("userns", sandbox.Userns)
|
writeFlag("userns", params.Userns)
|
||||||
writeFlag("net", sandbox.Net)
|
writeFlag("devel", params.Devel)
|
||||||
writeFlag("dev", sandbox.Dev)
|
writeFlag("net", params.HostNet)
|
||||||
writeFlag("tty", sandbox.Tty)
|
writeFlag("abstract", params.HostAbstract)
|
||||||
writeFlag("mapuid", sandbox.MapRealUID)
|
writeFlag("device", params.Device)
|
||||||
writeFlag("directwl", sandbox.DirectWayland)
|
writeFlag("tty", params.Tty)
|
||||||
writeFlag("autoetc", sandbox.AutoEtc)
|
writeFlag("mapuid", params.MapRealUID)
|
||||||
|
writeFlag("directwl", config.DirectWayland)
|
||||||
if len(flags) == 0 {
|
if len(flags) == 0 {
|
||||||
flags = append(flags, "none")
|
flags = append(flags, "none")
|
||||||
}
|
}
|
||||||
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
|
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
|
||||||
|
|
||||||
etc := sandbox.Etc
|
if config.Path != nil {
|
||||||
if etc == "" {
|
t.Printf(" Path:\t%s\n", config.Path)
|
||||||
etc = "/etc"
|
|
||||||
}
|
}
|
||||||
t.Printf(" Etc:\t%s\n", etc)
|
|
||||||
|
|
||||||
if len(sandbox.Cover) > 0 {
|
|
||||||
t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Env map[string]string `json:"env"`
|
|
||||||
// Link [][2]string `json:"symlink"`
|
|
||||||
}
|
}
|
||||||
t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
|
if len(config.Args) > 0 {
|
||||||
|
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
|
||||||
|
}
|
||||||
t.Printf("\n")
|
t.Printf("\n")
|
||||||
|
|
||||||
if !short {
|
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")
|
t.Printf("Filesystem\n")
|
||||||
for _, f := range config.Confinement.Sandbox.Filesystem {
|
for _, f := range config.Container.Filesystem {
|
||||||
if f == nil {
|
if !f.Valid() {
|
||||||
|
t.Println(" <invalid>")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
t.Printf(" %s\n", f)
|
||||||
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("\n")
|
t.Printf("\n")
|
||||||
}
|
}
|
||||||
if len(config.Confinement.ExtraPerms) > 0 {
|
if len(config.ExtraPerms) > 0 {
|
||||||
t.Printf("Extra ACL\n")
|
t.Printf("Extra ACL\n")
|
||||||
for _, p := range config.Confinement.ExtraPerms {
|
for _, p := range config.ExtraPerms {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -178,14 +151,14 @@ func printShowInstance(
|
|||||||
t.Printf(" Broadcast:\t%q\n", c.Broadcast)
|
t.Printf(" Broadcast:\t%q\n", c.Broadcast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config.Confinement.SessionBus != nil {
|
if config.SessionBus != nil {
|
||||||
t.Printf("Session bus\n")
|
t.Printf("Session bus\n")
|
||||||
printDBus(config.Confinement.SessionBus)
|
printDBus(config.SessionBus)
|
||||||
t.Printf("\n")
|
t.Printf("\n")
|
||||||
}
|
}
|
||||||
if config.Confinement.SystemBus != nil {
|
if config.SystemBus != nil {
|
||||||
t.Printf("System bus\n")
|
t.Printf("System bus\n")
|
||||||
printDBus(config.Confinement.SystemBus)
|
printDBus(config.SystemBus)
|
||||||
t.Printf("\n")
|
t.Printf("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,22 +220,26 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
|
|||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
|
||||||
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
|
t.Println("\tInstance\tPID\tApplication\tUptime")
|
||||||
for _, e := range exp {
|
for _, e := range exp {
|
||||||
var (
|
if len(e.s) != 1<<5 {
|
||||||
es = "(No confinement information)"
|
// unreachable
|
||||||
cs = "(No command information)"
|
log.Printf("possible store corruption: invalid instance string %s", e.s)
|
||||||
as = "(No configuration information)"
|
continue
|
||||||
)
|
|
||||||
if e.Config != nil {
|
|
||||||
es = e.Config.Confinement.Enablements.String()
|
|
||||||
cs = fmt.Sprintf("%q", e.Config.Args)
|
|
||||||
as = strconv.Itoa(e.Config.Confinement.AppID)
|
|
||||||
}
|
}
|
||||||
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 := "(No configuration information)"
|
||||||
|
if e.Config != nil {
|
||||||
|
as = strconv.Itoa(e.Config.Identity)
|
||||||
|
id := e.Config.ID
|
||||||
|
if id == "" {
|
||||||
|
id = "app.hakurei." + e.s[:8]
|
||||||
|
}
|
||||||
|
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 {
|
type expandedStateEntry struct {
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
'',
|
'',
|
||||||
|
|
||||||
id ? name,
|
id ? name,
|
||||||
app_id ? throw "app_id is required",
|
identity ? throw "identity is required",
|
||||||
groups ? [ ],
|
groups ? [ ],
|
||||||
userns ? false,
|
userns ? false,
|
||||||
net ? true,
|
net ? true,
|
||||||
@@ -57,7 +57,7 @@ let
|
|||||||
modules = modules ++ [
|
modules = modules ++ [
|
||||||
{
|
{
|
||||||
home = {
|
home = {
|
||||||
username = "fortify";
|
username = "hakurei";
|
||||||
homeDirectory = "/data/data/${id}";
|
homeDirectory = "/data/data/${id}";
|
||||||
stateVersion = "22.11";
|
stateVersion = "22.11";
|
||||||
};
|
};
|
||||||
@@ -65,7 +65,7 @@ let
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
launcher = writeScript "fortify-${pname}" ''
|
launcher = writeScript "hakurei-${pname}" ''
|
||||||
#!${runtimeShell} -el
|
#!${runtimeShell} -el
|
||||||
${script}
|
${script}
|
||||||
'';
|
'';
|
||||||
@@ -147,7 +147,7 @@ let
|
|||||||
name
|
name
|
||||||
version
|
version
|
||||||
id
|
id
|
||||||
app_id
|
identity
|
||||||
launcher
|
launcher
|
||||||
groups
|
groups
|
||||||
userns
|
userns
|
||||||
@@ -171,7 +171,12 @@ let
|
|||||||
broadcast = { };
|
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;
|
mesa = if gpu then mesaWrappers else null;
|
||||||
nix_gl = if gpu then nixGL else null;
|
nix_gl = if gpu then nixGL else null;
|
||||||
@@ -215,15 +220,14 @@ stdenv.mkDerivation {
|
|||||||
# create binary cache
|
# create binary cache
|
||||||
closureInfo="${
|
closureInfo="${
|
||||||
closureInfo {
|
closureInfo {
|
||||||
rootPaths =
|
rootPaths = [
|
||||||
[
|
homeManagerConfiguration.activationPackage
|
||||||
homeManagerConfiguration.activationPackage
|
launcher
|
||||||
launcher
|
]
|
||||||
]
|
++ optionals gpu [
|
||||||
++ optionals gpu [
|
mesaWrappers
|
||||||
mesaWrappers
|
nixGL
|
||||||
nixGL
|
];
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
echo "copying application paths..."
|
echo "copying application paths..."
|
||||||
@@ -10,39 +10,25 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/command"
|
"hakurei.app/command"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"hakurei.app/container"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"hakurei.app/hst"
|
||||||
"git.gensokyo.uk/security/fortify/internal/app"
|
"hakurei.app/internal"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"hakurei.app/internal/hlog"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const shellPath = "/run/current-system/sw/bin/bash"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errSuccess = errors.New("success")
|
errSuccess = errors.New("success")
|
||||||
|
|
||||||
std sys.State = new(sys.Std)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
fmsg.Prepare("fpkg")
|
hlog.Prepare("hpkg")
|
||||||
if err := os.Setenv("SHELL", shellPath); err != nil {
|
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
|
||||||
log.Fatalf("cannot set $SHELL: %v", err)
|
log.Fatalf("cannot set $SHELL: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
|
||||||
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
|
|
||||||
|
|
||||||
if err := sandbox.SetDumpable(sandbox.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 {
|
if os.Geteuid() == 0 {
|
||||||
log.Fatal("this program must not run as root")
|
log.Fatal("this program must not run as root")
|
||||||
}
|
}
|
||||||
@@ -55,14 +41,9 @@ func main() {
|
|||||||
flagVerbose bool
|
flagVerbose bool
|
||||||
flagDropShell bool
|
flagDropShell bool
|
||||||
)
|
)
|
||||||
c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
|
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||||
internal.InstallFmsg(flagVerbose)
|
|
||||||
return nil
|
|
||||||
}).
|
|
||||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
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 fortify action")
|
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
|
||||||
|
|
||||||
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
|
|
||||||
|
|
||||||
{
|
{
|
||||||
var (
|
var (
|
||||||
@@ -84,7 +65,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Look up paths to programs started by fpkg.
|
Look up paths to programs started by hpkg.
|
||||||
This is done here to ease error handling as cleanup is not yet required.
|
This is done here to ease error handling as cleanup is not yet required.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -99,31 +80,32 @@ func main() {
|
|||||||
Extract package and set up for cleanup.
|
Extract package and set up for cleanup.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var workDir string
|
var workDir *container.Absolute
|
||||||
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
|
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
|
||||||
log.Printf("cannot create temporary directory: %v", err)
|
log.Printf("cannot create temporary directory: %v", err)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else if workDir, err = container.NewAbs(p); err != nil {
|
||||||
workDir = p
|
log.Printf("invalid temporary directory: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
// should be faster than a native implementation
|
// should be faster than a native implementation
|
||||||
mustRun(chmod, "-R", "+w", workDir)
|
mustRun(chmod, "-R", "+w", workDir.String())
|
||||||
mustRun(rm, "-rf", workDir)
|
mustRun(rm, "-rf", workDir.String())
|
||||||
}
|
}
|
||||||
beforeRunFail.Store(&cleanup)
|
beforeRunFail.Store(&cleanup)
|
||||||
|
|
||||||
mustRun(tar, "-C", workDir, "-xf", pkgPath)
|
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Parse bundle and app metadata, do pre-install checks.
|
Parse bundle and app metadata, do pre-install checks.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
|
bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
|
||||||
pathSet := pathSetByApp(bundle.ID)
|
pathSet := pathSetByApp(bundle.ID)
|
||||||
|
|
||||||
a := bundle
|
a := bundle
|
||||||
if s, err := os.Stat(pathSet.metaPath); err != nil {
|
if s, err := os.Stat(pathSet.metaPath.String()); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
|
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
|
||||||
@@ -135,7 +117,7 @@ func main() {
|
|||||||
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
||||||
return syscall.EBADMSG
|
return syscall.EBADMSG
|
||||||
} else {
|
} else {
|
||||||
a = loadAppInfo(pathSet.metaPath, cleanup)
|
a = loadAppInfo(pathSet.metaPath.String(), cleanup)
|
||||||
if a.ID != bundle.ID {
|
if a.ID != bundle.ID {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Printf("app %q claims to have identifier %q",
|
log.Printf("app %q claims to have identifier %q",
|
||||||
@@ -157,19 +139,19 @@ func main() {
|
|||||||
return errSuccess
|
return errSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppID determines uid
|
// identity determines uid
|
||||||
if a.AppID != bundle.AppID {
|
if a.Identity != bundle.Identity {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Printf("package %q app id %d differs from installed %d",
|
log.Printf("package %q identity %d differs from installed %d",
|
||||||
pkgPath, bundle.AppID, a.AppID)
|
pkgPath, bundle.Identity, a.Identity)
|
||||||
return syscall.EBADE
|
return syscall.EBADE
|
||||||
}
|
}
|
||||||
|
|
||||||
// sec: should compare version string
|
// sec: should compare version string
|
||||||
fmsg.Verbosef("installing application %q version %q over local %q",
|
hlog.Verbosef("installing application %q version %q over local %q",
|
||||||
bundle.ID, bundle.Version, a.Version)
|
bundle.ID, bundle.Version, a.Version)
|
||||||
} else {
|
} else {
|
||||||
fmsg.Verbosef("application %q clean installation", bundle.ID)
|
hlog.Verbosef("application %q clean installation", bundle.ID)
|
||||||
// sec: should install credentials
|
// sec: should install credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +161,7 @@ func main() {
|
|||||||
|
|
||||||
withCacheDir(ctx, "install", []string{
|
withCacheDir(ctx, "install", []string{
|
||||||
// export inner bundle path in the environment
|
// export inner bundle path in the environment
|
||||||
"export BUNDLE=" + fst.Tmp + "/bundle",
|
"export BUNDLE=" + hst.Tmp + "/bundle",
|
||||||
// replace inner /etc
|
// replace inner /etc
|
||||||
"mkdir -p etc",
|
"mkdir -p etc",
|
||||||
"chmod -R +w etc",
|
"chmod -R +w etc",
|
||||||
@@ -218,7 +200,7 @@ func main() {
|
|||||||
"rm -rf .local/state/{nix,home-manager}",
|
"rm -rf .local/state/{nix,home-manager}",
|
||||||
// run activation script
|
// run activation script
|
||||||
bundle.ActivationPackage + "/activate",
|
bundle.ActivationPackage + "/activate",
|
||||||
}, false, func(config *fst.Config) *fst.Config { return config },
|
}, false, func(config *hst.Config) *hst.Config { return config },
|
||||||
bundle, pathSet, flagDropShellActivate, cleanup)
|
bundle, pathSet, flagDropShellActivate, cleanup)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -226,7 +208,7 @@ func main() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// serialise metadata to ensure consistency
|
// serialise metadata to ensure consistency
|
||||||
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Printf("cannot create metadata file: %v", err)
|
log.Printf("cannot create metadata file: %v", err)
|
||||||
return err
|
return err
|
||||||
@@ -239,7 +221,7 @@ func main() {
|
|||||||
// not fatal
|
// not fatal
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
|
if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Printf("cannot rename metadata file: %v", err)
|
log.Printf("cannot rename metadata file: %v", err)
|
||||||
return err
|
return err
|
||||||
@@ -268,7 +250,7 @@ func main() {
|
|||||||
|
|
||||||
id := args[0]
|
id := args[0]
|
||||||
pathSet := pathSetByApp(id)
|
pathSet := pathSetByApp(id)
|
||||||
a := loadAppInfo(pathSet.metaPath, func() {})
|
a := loadAppInfo(pathSet.metaPath.String(), func() {})
|
||||||
if a.ID != id {
|
if a.ID != id {
|
||||||
log.Printf("app %q claims to have identifier %q", id, a.ID)
|
log.Printf("app %q claims to have identifier %q", id, a.ID)
|
||||||
return syscall.EBADE
|
return syscall.EBADE
|
||||||
@@ -291,14 +273,14 @@ func main() {
|
|||||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||||
"path:" + a.NixGL + "#nixVulkanNvidia",
|
"path:" + a.NixGL + "#nixVulkanNvidia",
|
||||||
}, true, func(config *fst.Config) *fst.Config {
|
}, true, func(config *hst.Config) *hst.Config {
|
||||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||||
{Src: "/etc/resolv.conf"},
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
|
||||||
{Src: "/sys/block"},
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
|
||||||
{Src: "/sys/bus"},
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
|
||||||
{Src: "/sys/class"},
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
|
||||||
{Src: "/sys/dev"},
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
|
||||||
{Src: "/sys/devices"},
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
|
||||||
}...)
|
}...)
|
||||||
appendGPUFilesystem(config)
|
appendGPUFilesystem(config)
|
||||||
return config
|
return config
|
||||||
@@ -309,23 +291,24 @@ func main() {
|
|||||||
Create app configuration.
|
Create app configuration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pathname := a.Launcher
|
||||||
argv := make([]string, 1, len(args))
|
argv := make([]string, 1, len(args))
|
||||||
if !flagDropShell {
|
if flagDropShell {
|
||||||
argv[0] = a.Launcher
|
pathname = pathShell
|
||||||
|
argv[0] = bash
|
||||||
} else {
|
} else {
|
||||||
argv[0] = shellPath
|
argv[0] = a.Launcher.String()
|
||||||
}
|
}
|
||||||
argv = append(argv, args[1:]...)
|
argv = append(argv, args[1:]...)
|
||||||
|
config := a.toHst(pathSet, pathname, argv, flagDropShell)
|
||||||
config := a.toFst(pathSet, argv, flagDropShell)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Expose GPU devices.
|
Expose GPU devices.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if a.GPU {
|
if a.GPU {
|
||||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
|
config.Container.Filesystem = append(config.Container.Filesystem,
|
||||||
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
|
||||||
appendGPUFilesystem(config)
|
appendGPUFilesystem(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,9 +324,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.MustParse(os.Args[1:], func(err error) {
|
c.MustParse(os.Args[1:], func(err error) {
|
||||||
fmsg.Verbosef("command returned %v", err)
|
hlog.Verbosef("command returned %v", err)
|
||||||
if errors.Is(err, errSuccess) {
|
if errors.Is(err, errSuccess) {
|
||||||
fmsg.BeforeExit()
|
hlog.BeforeExit()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,11 +50,13 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
environment.fortify = {
|
environment.hakurei = {
|
||||||
enable = true;
|
enable = true;
|
||||||
stateDir = "/var/lib/fortify";
|
stateDir = "/var/lib/hakurei";
|
||||||
users.alice = 0;
|
users.alice = 0;
|
||||||
|
|
||||||
home-manager = _: _: { home.stateVersion = "23.05"; };
|
extraHomeConfig = {
|
||||||
|
home.stateVersion = "23.05";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ let
|
|||||||
buildPackage = self.buildPackage.${system};
|
buildPackage = self.buildPackage.${system};
|
||||||
in
|
in
|
||||||
nixosTest {
|
nixosTest {
|
||||||
name = "fpkg";
|
name = "hpkg";
|
||||||
nodes.machine = {
|
nodes.machine = {
|
||||||
environment.etc = {
|
environment.etc = {
|
||||||
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
|
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
|
||||||
@@ -18,7 +18,7 @@ nixosTest {
|
|||||||
imports = [
|
imports = [
|
||||||
./configuration.nix
|
./configuration.nix
|
||||||
|
|
||||||
self.nixosModules.fortify
|
self.nixosModules.hakurei
|
||||||
self.inputs.home-manager.nixosModules.home-manager
|
self.inputs.home-manager.nixosModules.home-manager
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -10,7 +10,7 @@ buildPackage {
|
|||||||
name = "foot";
|
name = "foot";
|
||||||
inherit (foot) version;
|
inherit (foot) version;
|
||||||
|
|
||||||
app_id = 2;
|
identity = 2;
|
||||||
id = "org.codeberg.dnkl.foot";
|
id = "org.codeberg.dnkl.foot";
|
||||||
|
|
||||||
modules = [
|
modules = [
|
||||||
@@ -47,52 +47,52 @@ def wait_for_window(pattern):
|
|||||||
|
|
||||||
|
|
||||||
def collect_state_ui(name):
|
def collect_state_ui(name):
|
||||||
swaymsg(f"exec fortify ps > '/tmp/{name}.ps'")
|
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
|
||||||
machine.copy_from_vm(f"/tmp/{name}.ps", "")
|
machine.copy_from_vm(f"/tmp/{name}.ps", "")
|
||||||
swaymsg(f"exec fortify --json ps > '/tmp/{name}.json'")
|
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
|
||||||
machine.copy_from_vm(f"/tmp/{name}.json", "")
|
machine.copy_from_vm(f"/tmp/{name}.json", "")
|
||||||
machine.screenshot(name)
|
machine.screenshot(name)
|
||||||
|
|
||||||
|
|
||||||
def check_state(name, enablements):
|
def check_state(name, enablements):
|
||||||
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
|
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
|
||||||
if len(instances) != 1:
|
if len(instances) != 1:
|
||||||
raise Exception(f"unexpected state length {len(instances)}")
|
raise Exception(f"unexpected state length {len(instances)}")
|
||||||
instance = next(iter(instances.values()))
|
instance = next(iter(instances.values()))
|
||||||
|
|
||||||
config = instance['config']
|
config = instance['config']
|
||||||
|
|
||||||
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['args'][0]):
|
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']}")
|
raise Exception(f"unexpected args {instance['config']['args']}")
|
||||||
|
|
||||||
if config['confinement']['enablements'] != enablements:
|
if config['enablements'] != enablements:
|
||||||
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
|
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
|
||||||
|
|
||||||
|
|
||||||
start_all()
|
start_all()
|
||||||
machine.wait_for_unit("multi-user.target")
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
# To check fortify's version:
|
# To check hakurei's version:
|
||||||
print(machine.succeed("sudo -u alice -i fortify version"))
|
print(machine.succeed("sudo -u alice -i hakurei version"))
|
||||||
|
|
||||||
# Wait for Sway to complete startup:
|
# Wait for Sway to complete startup:
|
||||||
machine.wait_for_file("/run/user/1000/wayland-1")
|
machine.wait_for_file("/run/user/1000/wayland-1")
|
||||||
machine.wait_for_file("/tmp/sway-ipc.sock")
|
machine.wait_for_file("/tmp/sway-ipc.sock")
|
||||||
|
|
||||||
# Prepare fpkg directory:
|
# Prepare hpkg directory:
|
||||||
machine.succeed("install -dm 0700 -o alice -g users /var/lib/fortify/1000")
|
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
|
||||||
|
|
||||||
# Install fpkg app:
|
# Install hpkg app:
|
||||||
swaymsg("exec fpkg -v install /etc/foot.pkg && touch /tmp/fpkg-install-done")
|
swaymsg("exec hpkg -v install /etc/foot.pkg && touch /tmp/hpkg-install-ok")
|
||||||
machine.wait_for_file("/tmp/fpkg-install-done")
|
machine.wait_for_file("/tmp/hpkg-install-ok")
|
||||||
|
|
||||||
# Start app (foot) with Wayland enablement:
|
# Start app (foot) with Wayland enablement:
|
||||||
swaymsg("exec fpkg -v start org.codeberg.dnkl.foot")
|
swaymsg("exec hpkg -v start org.codeberg.dnkl.foot")
|
||||||
wait_for_window("fortify@machine-foot")
|
wait_for_window("hakurei@machine-foot")
|
||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client")
|
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
|
||||||
collect_state_ui("app_wayland")
|
collect_state_ui("app_wayland")
|
||||||
check_state("foot", 13)
|
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
|
||||||
# Verify acl on XDG_RUNTIME_DIR:
|
# Verify acl on XDG_RUNTIME_DIR:
|
||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
@@ -104,5 +104,5 @@ machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/
|
|||||||
swaymsg("exit", succeed=False)
|
swaymsg("exit", succeed=False)
|
||||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||||
|
|
||||||
# Print fortify runDir contents:
|
# Print hakurei runDir contents:
|
||||||
print(machine.succeed("find /run/user/1000/fortify"))
|
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,17 +13,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
fsuConfFile = "/etc/fsurc"
|
hsuConfFile = "/etc/hsurc"
|
||||||
envShim = "FORTIFY_SHIM"
|
envShim = "HAKUREI_SHIM"
|
||||||
envAID = "FORTIFY_APP_ID"
|
envIdentity = "HAKUREI_IDENTITY"
|
||||||
envGroups = "FORTIFY_GROUPS"
|
envGroups = "HAKUREI_GROUPS"
|
||||||
|
|
||||||
PR_SET_NO_NEW_PRIVS = 0x26
|
PR_SET_NO_NEW_PRIVS = 0x26
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
log.SetPrefix("fsu: ")
|
log.SetPrefix("hsu: ")
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
|
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
@@ -40,58 +40,63 @@ func main() {
|
|||||||
if p, err := os.Readlink(pexe); err != nil {
|
if p, err := os.Readlink(pexe); err != nil {
|
||||||
log.Fatalf("cannot read parent executable path: %v", err)
|
log.Fatalf("cannot read parent executable path: %v", err)
|
||||||
} else if strings.HasSuffix(p, " (deleted)") {
|
} else if strings.HasSuffix(p, " (deleted)") {
|
||||||
log.Fatal("fortify executable has been deleted")
|
log.Fatal("hakurei executable has been deleted")
|
||||||
} else if p != mustCheckPath(fmain) && p != mustCheckPath(fpkg) {
|
} else if p != mustCheckPath(hmain) {
|
||||||
log.Fatal("this program must be started by fortify")
|
log.Fatal("this program must be started by hakurei")
|
||||||
} else {
|
} else {
|
||||||
toolPath = p
|
toolPath = p
|
||||||
}
|
}
|
||||||
|
|
||||||
// uid = 1000000 +
|
// uid = 1000000 +
|
||||||
// fid * 10000 +
|
// id * 10000 +
|
||||||
// aid
|
// identity
|
||||||
uid := 1000000
|
uid := 1000000
|
||||||
|
|
||||||
// refuse to run if fsurc is not protected correctly
|
// refuse to run if hsurc is not protected correctly
|
||||||
if s, err := os.Stat(fsuConfFile); err != nil {
|
if s, err := os.Stat(hsuConfFile); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
} else if s.Mode().Perm() != 0400 {
|
} 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 {
|
} 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
|
// authenticate before accepting user input
|
||||||
if f, err := os.Open(fsuConfFile); err != nil {
|
var id int
|
||||||
|
if f, err := os.Open(hsuConfFile); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
} else if fid, ok := mustParseConfig(f, puid); !ok {
|
} else if v, 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 {
|
} else {
|
||||||
uid += fid * 10000
|
id = v
|
||||||
}
|
if err = f.Close(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// allowed aid range 0 to 9999
|
uid += id * 10000
|
||||||
if as, ok := os.LookupEnv(envAID); !ok {
|
|
||||||
log.Fatal("FORTIFY_APP_ID not set")
|
|
||||||
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
|
|
||||||
log.Fatal("invalid aid")
|
|
||||||
} else {
|
|
||||||
uid += aid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass through setup fd to shim
|
// pass through setup fd to shim
|
||||||
var shimSetupFd string
|
var shimSetupFd string
|
||||||
if s, ok := os.LookupEnv(envShim); !ok {
|
if s, ok := os.LookupEnv(envShim); !ok {
|
||||||
// fortify requests target uid
|
// hakurei requests hsurc user id
|
||||||
// print resolved uid and exit
|
fmt.Print(id)
|
||||||
fmt.Print(uid)
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
} 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 {
|
} else {
|
||||||
shimSetupFd = s
|
shimSetupFd = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allowed identity range 0 to 9999
|
||||||
|
if as, ok := os.LookupEnv(envIdentity); !ok {
|
||||||
|
log.Fatal("HAKUREI_IDENTITY not set")
|
||||||
|
} else if identity, err := parseUint32Fast(as); err != nil || identity < 0 || identity > 9999 {
|
||||||
|
log.Fatal("invalid identity")
|
||||||
|
} else {
|
||||||
|
uid += identity
|
||||||
|
}
|
||||||
|
|
||||||
// supplementary groups
|
// supplementary groups
|
||||||
var suppGroups, suppCurrent []int
|
var suppGroups, suppCurrent []int
|
||||||
|
|
||||||
@@ -124,7 +129,7 @@ func main() {
|
|||||||
panic("uid out of bounds")
|
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 {
|
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
||||||
log.Fatalf("cannot set gid: %v", err)
|
log.Fatalf("cannot set gid: %v", err)
|
||||||
@@ -138,7 +143,7 @@ func main() {
|
|||||||
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
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())
|
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||||
}
|
}
|
||||||
if err := syscall.Exec(toolPath, []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)
|
log.Fatalf("cannot start shim: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if ok {
|
||||||
// allowed fid range 0 to 99
|
// allowed fid range 0 to 99
|
||||||
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 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
|
return
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ func Test_parseConfig(t *testing.T) {
|
|||||||
{"empty", 0, -1, "", ``},
|
{"empty", 0, -1, "", ``},
|
||||||
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
||||||
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 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`},
|
{"match", 1000, 0, "", `1000 0`},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,8 +8,7 @@ import (
|
|||||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fmain = compPoison
|
hmain = compPoison
|
||||||
fpkg = compPoison
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustCheckPath(p string) string {
|
func mustCheckPath(p string) string {
|
||||||
@@ -3,7 +3,7 @@ package command_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/command"
|
"hakurei.app/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuild(t *testing.T) {
|
func TestBuild(t *testing.T) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/command"
|
"hakurei.app/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
68
container/autoetc.go
Normal file
68
container/autoetc.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 OpRepeatError("autoetc")
|
||||||
|
}
|
||||||
|
state.nonrepeatable |= nrAutoEtc
|
||||||
|
|
||||||
|
const target = sysrootPath + FHSEtc
|
||||||
|
rel := e.hostRel() + "/"
|
||||||
|
|
||||||
|
if err := k.mkdirAll(target, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
|
||||||
|
return 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 err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if err = k.symlink(rel+n, target+n); err != nil {
|
||||||
|
return 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, bool) { return "setting up", true }
|
||||||
|
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||||
292
container/autoetc_test.go
Normal file
292
container/autoetc_test.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAutoEtcOp(t *testing.T) {
|
||||||
|
t.Run("nonrepeatable", func(t *testing.T) {
|
||||||
|
wantErr := OpRepeatError("autoetc")
|
||||||
|
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, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, stub.UniqueError(3)),
|
||||||
|
}, stub.UniqueError(3)},
|
||||||
|
|
||||||
|
{"readdir", new(Params), &AutoEtcOp{
|
||||||
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), stub.UniqueError(2)),
|
||||||
|
}, stub.UniqueError(2)},
|
||||||
|
|
||||||
|
{"symlink", new(Params), &AutoEtcOp{
|
||||||
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("readdir", stub.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),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, stub.UniqueError(1)),
|
||||||
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
|
{"symlink mtab", new(Params), &AutoEtcOp{
|
||||||
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("readdir", stub.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),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, stub.UniqueError(0)),
|
||||||
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
|
{"success nested", new(Params), &AutoEtcOp{
|
||||||
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("readdir", stub.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),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"success", new(Params), &AutoEtcOp{
|
||||||
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("readdir", stub.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),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
|
||||||
|
call("symlink", stub.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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 []*BindMountOp
|
||||||
|
}
|
||||||
|
|
||||||
|
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 err
|
||||||
|
} else {
|
||||||
|
r.resolved = make([]*BindMountOp, 0, len(d))
|
||||||
|
for _, ent := range d {
|
||||||
|
name := ent.Name()
|
||||||
|
if IsAutoRootBindable(name) {
|
||||||
|
// careful: the Valid method is skipped, make sure this is always valid
|
||||||
|
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 OpRepeatError("autoroot")
|
||||||
|
}
|
||||||
|
state.nonrepeatable |= nrAutoRoot
|
||||||
|
|
||||||
|
for _, op := range r.resolved {
|
||||||
|
// these are exclusively BindMountOp, do not attempt to print identifying message
|
||||||
|
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, bool) { return "setting up", true }
|
||||||
|
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"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAutoRootOp(t *testing.T) {
|
||||||
|
t.Run("nonrepeatable", func(t *testing.T) {
|
||||||
|
wantErr := OpRepeatError("autoroot")
|
||||||
|
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,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
|
||||||
|
}, stub.UniqueError(2), nil, nil},
|
||||||
|
|
||||||
|
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
|
Host: MustAbs("/"),
|
||||||
|
Flags: BindWritable,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "", stub.UniqueError(1)),
|
||||||
|
}, stub.UniqueError(1), nil, nil},
|
||||||
|
|
||||||
|
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
|
Host: MustAbs("/"),
|
||||||
|
Flags: BindWritable,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(false), stub.UniqueError(0)),
|
||||||
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
|
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
|
Host: MustAbs("/"),
|
||||||
|
Flags: BindWritable,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/home", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lib64", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lost+found", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/nix", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/root", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/run", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/srv", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/sys", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/usr", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/var", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil),
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
|
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("readdir", stub.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),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.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: []*BindMountOp{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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
409
container/container.go
Normal file
409
container/container.go
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
// 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 processes lingering after the initial process terminates.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// A StartError contains additional information on a container startup failure.
|
||||||
|
type StartError struct {
|
||||||
|
// Fatal suggests whether this error should be considered fatal for the entire program.
|
||||||
|
Fatal bool
|
||||||
|
// Step refers to the part of the setup this error is returned from.
|
||||||
|
Step string
|
||||||
|
// Err is the underlying error.
|
||||||
|
Err error
|
||||||
|
// Origin is whether this error originated from the [Container.Start] method.
|
||||||
|
Origin bool
|
||||||
|
// Passthrough is whether the Error method is passed through to Err.
|
||||||
|
Passthrough bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *StartError) Unwrap() error { return e.Err }
|
||||||
|
func (e *StartError) Error() string {
|
||||||
|
if e.Passthrough {
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
if e.Origin {
|
||||||
|
return e.Step
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var syscallError *os.SyscallError
|
||||||
|
if errors.As(e.Err, &syscallError) && syscallError != nil {
|
||||||
|
return e.Step + " " + syscallError.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Step + ": " + e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message returns a user-facing error message.
|
||||||
|
func (e *StartError) Message() string {
|
||||||
|
if e.Passthrough {
|
||||||
|
switch {
|
||||||
|
case errors.As(e.Err, new(*os.PathError)),
|
||||||
|
errors.As(e.Err, new(*os.SyscallError)):
|
||||||
|
return "cannot " + e.Err.Error()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Origin {
|
||||||
|
return e.Step
|
||||||
|
}
|
||||||
|
return "cannot " + e.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the container init. The init process blocks until Serve is called.
|
||||||
|
func (p *Container) Start() error {
|
||||||
|
if p == nil || p.cmd == nil ||
|
||||||
|
p.Ops == nil || len(*p.Ops) == 0 {
|
||||||
|
return errors.New("container: starting an invalid container")
|
||||||
|
}
|
||||||
|
if p.cmd.Process != nil {
|
||||||
|
return errors.New("container: already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.cmd.Stdin == nil {
|
||||||
|
p.cmd.Stdin = p.Stdin
|
||||||
|
}
|
||||||
|
if p.cmd.Stdout == nil {
|
||||||
|
p.cmd.Stdout = p.Stdout
|
||||||
|
}
|
||||||
|
if p.cmd.Stderr == nil {
|
||||||
|
p.cmd.Stderr = p.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cmd.Args = []string{initName}
|
||||||
|
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 &StartError{true, "set up params stream", err, false, false}
|
||||||
|
} 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 &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 &StartError{false, "get landlock ABI", err, false, false}
|
||||||
|
} else if abi < 6 {
|
||||||
|
if p.HostAbstract {
|
||||||
|
// see above comment
|
||||||
|
goto landlockOut
|
||||||
|
}
|
||||||
|
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
|
||||||
|
} else {
|
||||||
|
msg.Verbosef("landlock abi version %d", abi)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
||||||
|
return &StartError{true, "create landlock ruleset", err, false, false}
|
||||||
|
} else {
|
||||||
|
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
||||||
|
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
||||||
|
_ = Close(rulesetFd)
|
||||||
|
return &StartError{true, "enforce landlock ruleset", err, false, false}
|
||||||
|
}
|
||||||
|
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 &StartError{false, "start container init", err, false, true}
|
||||||
|
}
|
||||||
|
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 &StartError{false, "invalid executable pathname", EINVAL, true, false}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 || p.cmd.Process == nil {
|
||||||
|
return EINVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.cmd.Wait()
|
||||||
|
p.cancel()
|
||||||
|
if p.wait != nil && err == nil {
|
||||||
|
close(p.wait)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdinPipe calls the [exec.Cmd] method with the same name.
|
||||||
|
func (p *Container) StdinPipe() (w io.WriteCloser, err error) {
|
||||||
|
if p.Stdin != nil {
|
||||||
|
return nil, errors.New("container: Stdin already set")
|
||||||
|
}
|
||||||
|
w, err = p.cmd.StdinPipe()
|
||||||
|
p.Stdin = p.cmd.Stdin
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdoutPipe calls the [exec.Cmd] method with the same name.
|
||||||
|
func (p *Container) StdoutPipe() (r io.ReadCloser, err error) {
|
||||||
|
if p.Stdout != nil {
|
||||||
|
return nil, errors.New("container: Stdout already set")
|
||||||
|
}
|
||||||
|
r, err = p.cmd.StdoutPipe()
|
||||||
|
p.Stdout = p.cmd.Stdout
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StderrPipe calls the [exec.Cmd] method with the same name.
|
||||||
|
func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
|
||||||
|
if p.Stderr != nil {
|
||||||
|
return nil, errors.New("container: Stderr already set")
|
||||||
|
}
|
||||||
|
r, err = p.cmd.StderrPipe()
|
||||||
|
p.Stderr = p.cmd.Stderr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
|
||||||
|
c, cancel := context.WithCancel(ctx)
|
||||||
|
p.cancel = cancel
|
||||||
|
p.cmd = exec.CommandContext(c, MustExecutable())
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
734
container/container_test.go
Normal file
734
container/container_test.go
Normal file
@@ -0,0 +1,734 @@
|
|||||||
|
package container_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"reflect"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStartError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
s string
|
||||||
|
is error
|
||||||
|
isF error
|
||||||
|
msg string
|
||||||
|
}{
|
||||||
|
{"params env", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "set up params stream",
|
||||||
|
Err: container.ErrReceiveEnv,
|
||||||
|
},
|
||||||
|
"set up params stream: environment variable not set",
|
||||||
|
container.ErrReceiveEnv, syscall.EBADF,
|
||||||
|
"cannot set up params stream: environment variable not set"},
|
||||||
|
|
||||||
|
{"params", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "set up params stream",
|
||||||
|
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
|
||||||
|
},
|
||||||
|
"set up params stream pipe2: bad file descriptor",
|
||||||
|
syscall.EBADF, os.ErrInvalid,
|
||||||
|
"cannot set up params stream pipe2: bad file descriptor"},
|
||||||
|
|
||||||
|
{"PR_SET_NO_NEW_PRIVS", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||||
|
Err: syscall.EPERM,
|
||||||
|
},
|
||||||
|
"prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
|
||||||
|
syscall.EPERM, syscall.EACCES,
|
||||||
|
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
|
||||||
|
|
||||||
|
{"landlock abi", &container.StartError{
|
||||||
|
Step: "get landlock ABI",
|
||||||
|
Err: syscall.ENOSYS,
|
||||||
|
},
|
||||||
|
"get landlock ABI: function not implemented",
|
||||||
|
syscall.ENOSYS, syscall.ENOEXEC,
|
||||||
|
"cannot get landlock ABI: function not implemented"},
|
||||||
|
|
||||||
|
{"landlock old", &container.StartError{
|
||||||
|
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||||
|
Err: syscall.ENOSYS,
|
||||||
|
Origin: true,
|
||||||
|
},
|
||||||
|
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||||
|
syscall.ENOSYS, syscall.ENOSPC,
|
||||||
|
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
|
||||||
|
|
||||||
|
{"landlock create", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "create landlock ruleset",
|
||||||
|
Err: syscall.EBADFD,
|
||||||
|
},
|
||||||
|
"create landlock ruleset: file descriptor in bad state",
|
||||||
|
syscall.EBADFD, syscall.EBADF,
|
||||||
|
"cannot create landlock ruleset: file descriptor in bad state"},
|
||||||
|
|
||||||
|
{"landlock enforce", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "enforce landlock ruleset",
|
||||||
|
Err: syscall.ENOTRECOVERABLE,
|
||||||
|
},
|
||||||
|
"enforce landlock ruleset: state not recoverable",
|
||||||
|
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
|
||||||
|
"cannot enforce landlock ruleset: state not recoverable"},
|
||||||
|
|
||||||
|
{"start", &container.StartError{
|
||||||
|
Step: "start container init",
|
||||||
|
Err: &os.PathError{
|
||||||
|
Op: "fork/exec",
|
||||||
|
Path: "/proc/nonexistent",
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}, Passthrough: true,
|
||||||
|
},
|
||||||
|
"fork/exec /proc/nonexistent: no such file or directory",
|
||||||
|
syscall.ENOENT, syscall.ENOSYS,
|
||||||
|
"cannot fork/exec /proc/nonexistent: no such file or directory"},
|
||||||
|
|
||||||
|
{"start syscall", &container.StartError{
|
||||||
|
Step: "start container init",
|
||||||
|
Err: &os.SyscallError{
|
||||||
|
Syscall: "open",
|
||||||
|
Err: syscall.ENOSYS,
|
||||||
|
}, Passthrough: true,
|
||||||
|
},
|
||||||
|
"open: function not implemented",
|
||||||
|
syscall.ENOSYS, syscall.ENOENT,
|
||||||
|
"cannot open: function not implemented"},
|
||||||
|
|
||||||
|
{"start other", &container.StartError{
|
||||||
|
Step: "start container init",
|
||||||
|
Err: &net.OpError{
|
||||||
|
Op: "dial",
|
||||||
|
Net: "unix",
|
||||||
|
Err: syscall.ECONNREFUSED,
|
||||||
|
}, Passthrough: true,
|
||||||
|
},
|
||||||
|
"dial unix: connection refused",
|
||||||
|
syscall.ECONNREFUSED, syscall.ECONNABORTED,
|
||||||
|
"dial unix: connection refused"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Run("error", func(t *testing.T) {
|
||||||
|
if got := tc.err.Error(); got != tc.s {
|
||||||
|
t.Errorf("Error: %q, want %q", got, tc.s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
if !errors.Is(tc.err, tc.is) {
|
||||||
|
t.Error("Is: unexpected false")
|
||||||
|
}
|
||||||
|
if errors.Is(tc.err, tc.isF) {
|
||||||
|
t.Errorf("Is: unexpected true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("msg", func(t *testing.T) {
|
||||||
|
if got, ok := container.GetErrorMessage(tc.err); !ok {
|
||||||
|
if tc.msg != "" {
|
||||||
|
t.Errorf("GetErrorMessage: err does not implement MessageError")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if got != tc.msg {
|
||||||
|
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 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"),
|
||||||
|
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||||
|
),
|
||||||
|
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"),
|
||||||
|
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||||
|
),
|
||||||
|
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(); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Error(m)
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Error(m)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Fatal(m)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("cannot start container: %v", err)
|
||||||
|
}
|
||||||
|
} else if err = c.Serve(); err != nil {
|
||||||
|
_, _ = output.WriteTo(os.Stdout)
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Error(m)
|
||||||
|
} else {
|
||||||
|
t.Errorf("cannot serve setup params: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.Wait(); err != nil {
|
||||||
|
_, _ = output.WriteTo(os.Stdout)
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Fatal(m)
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Fatal(m)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("cannot start container: %v", err)
|
||||||
|
}
|
||||||
|
} else if err = c.Serve(); err != nil {
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Error(m)
|
||||||
|
} else {
|
||||||
|
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...)
|
||||||
|
}
|
||||||
242
container/dispatcher.go
Normal file
242
container/dispatcher.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
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) 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) error {
|
||||||
|
return hostProc.bindMount(source, target, flags)
|
||||||
|
}
|
||||||
|
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 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() }
|
||||||
744
container/dispatcher_test.go
Normal file
744
container/dispatcher_test.go
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/container/seccomp"
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
type opValidTestCase struct {
|
||||||
|
name string
|
||||||
|
op Op
|
||||||
|
want bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
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.Helper()
|
||||||
|
|
||||||
|
t.Run("build", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
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.Helper()
|
||||||
|
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
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.Helper()
|
||||||
|
|
||||||
|
t.Run("meta", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
t.Run("prefix", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if got, _ := tc.op.prefix(); got != tc.wantPrefix {
|
||||||
|
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if got := tc.op.String(); got != tc.wantString {
|
||||||
|
t.Errorf("String: %s, want %s", got, tc.wantString)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// call initialises a [stub.Call].
|
||||||
|
// This keeps composites analysis happy without making the test cases too bloated.
|
||||||
|
func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
|
||||||
|
return stub.Call{Name: name, Args: args, Ret: ret, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
type simpleTestCase struct {
|
||||||
|
name string
|
||||||
|
f func(k syscallDispatcher) error
|
||||||
|
want stub.Expect
|
||||||
|
wantErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
wait4signal := make(chan struct{})
|
||||||
|
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
|
||||||
|
defer stub.HandleExit(t)
|
||||||
|
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
|
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
||||||
|
}
|
||||||
|
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type opBehaviourTestCase struct {
|
||||||
|
name string
|
||||||
|
params *Params
|
||||||
|
op Op
|
||||||
|
|
||||||
|
early []stub.Call
|
||||||
|
wantErrEarly error
|
||||||
|
|
||||||
|
apply []stub.Call
|
||||||
|
wantErrApply error
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
t.Run("behaviour", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
state := &setupState{Params: tc.params}
|
||||||
|
k := &kstub{nil, stub.New(t,
|
||||||
|
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
|
||||||
|
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
||||||
|
)}
|
||||||
|
defer stub.HandleExit(t)
|
||||||
|
errEarly := tc.op.early(state, k)
|
||||||
|
k.Expects(stub.CallSeparator)
|
||||||
|
if !reflect.DeepEqual(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); !reflect.DeepEqual(err, tc.wantErrApply) {
|
||||||
|
t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply)
|
||||||
|
}
|
||||||
|
|
||||||
|
out:
|
||||||
|
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
|
||||||
|
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 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") }
|
||||||
|
|
||||||
|
const (
|
||||||
|
// magicWait4Signal must be used in a single pair of signal and wait4 calls across two goroutines
|
||||||
|
// originating from the same toplevel kstub.
|
||||||
|
// To enable this behaviour this value must be the last element of the args field in the wait4 call
|
||||||
|
// and the ret value of the signal call.
|
||||||
|
magicWait4Signal = 0xdef
|
||||||
|
)
|
||||||
|
|
||||||
|
type kstub struct {
|
||||||
|
wait4signal chan struct{}
|
||||||
|
*stub.Stub[syscallDispatcher]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
||||||
|
|
||||||
|
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
|
||||||
|
|
||||||
|
func (k *kstub) setPtracer(pid uintptr) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("setPtracer").Error(
|
||||||
|
stub.CheckArg(k.Stub, "pid", pid, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) setDumpable(dumpable uintptr) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("setDumpable").Error(
|
||||||
|
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
|
||||||
|
func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) }
|
||||||
|
|
||||||
|
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("capset").Error(
|
||||||
|
stub.CheckArgReflect(k.Stub, "hdrp", hdrp, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "datap", datap, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) capBoundingSetDrop(cap uintptr) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("capBoundingSetDrop").Error(
|
||||||
|
stub.CheckArg(k.Stub, "cap", cap, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) capAmbientClearAll() error { k.Helper(); return k.Expects("capAmbientClearAll").Err }
|
||||||
|
|
||||||
|
func (k *kstub) capAmbientRaise(cap uintptr) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("capAmbientRaise").Error(
|
||||||
|
stub.CheckArg(k.Stub, "cap", cap, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) isatty(fd int) bool {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("isatty")
|
||||||
|
if !stub.CheckArg(k.Stub, "fd", fd, 0) {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
return expect.Ret.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("receive")
|
||||||
|
|
||||||
|
var closed bool
|
||||||
|
closeFunc = func() error {
|
||||||
|
if closed {
|
||||||
|
k.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(
|
||||||
|
stub.CheckArg(k.Stub, "key", key, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
||||||
|
stub.CheckArgReflect(k.Stub, "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) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("bindMount").Error(
|
||||||
|
stub.CheckArg(k.Stub, "source", source, 0),
|
||||||
|
stub.CheckArg(k.Stub, "target", target, 1),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) remount(target string, flags uintptr) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("remount").Error(
|
||||||
|
stub.CheckArg(k.Stub, "target", target, 0),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("mountTmpfs").Error(
|
||||||
|
stub.CheckArg(k.Stub, "fsname", fsname, 0),
|
||||||
|
stub.CheckArg(k.Stub, "target", target, 1),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 2),
|
||||||
|
stub.CheckArg(k.Stub, "size", size, 3),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("ensureFile").Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 1),
|
||||||
|
stub.CheckArg(k.Stub, "pperm", pperm, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("seccompLoad").Error(
|
||||||
|
stub.CheckArgReflect(k.Stub, "rules", rules, 0),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("notify")
|
||||||
|
if c == nil || expect.Error(
|
||||||
|
stub.CheckArgReflect(k.Stub, "sig", sig, 1)) != nil {
|
||||||
|
k.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 {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("start")
|
||||||
|
err := expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
|
||||||
|
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
|
||||||
|
stub.CheckArg(k.Stub, "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 {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("signal")
|
||||||
|
if v, ok := expect.Ret.(int); ok && v == magicWait4Signal {
|
||||||
|
if k.wait4signal == nil {
|
||||||
|
panic("kstub not initialised for wait4 simulation")
|
||||||
|
}
|
||||||
|
defer func() { close(k.wait4signal) }()
|
||||||
|
}
|
||||||
|
return expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
|
||||||
|
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
|
||||||
|
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3),
|
||||||
|
stub.CheckArg(k.Stub, "sig", sig, 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) evalSymlinks(path string) (string, error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("evalSymlinks")
|
||||||
|
return expect.Ret.(string), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "path", path, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) exit(code int) {
|
||||||
|
k.Helper()
|
||||||
|
k.Expects("exit")
|
||||||
|
if !stub.CheckArg(k.Stub, "code", code, 0) {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
panic(stub.PanicExit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) getpid() int { k.Helper(); return k.Expects("getpid").Ret.(int) }
|
||||||
|
|
||||||
|
func (k *kstub) stat(name string) (os.FileInfo, error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("stat")
|
||||||
|
return expect.Ret.(os.FileInfo), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) mkdir(name string, perm os.FileMode) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("mkdir").Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) mkdirTemp(dir, pattern string) (string, error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("mkdirTemp")
|
||||||
|
return expect.Ret.(string), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "dir", dir, 0),
|
||||||
|
stub.CheckArg(k.Stub, "pattern", pattern, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) mkdirAll(path string, perm os.FileMode) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("mkdirAll").Error(
|
||||||
|
stub.CheckArg(k.Stub, "path", path, 0),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("readdir")
|
||||||
|
return expect.Ret.([]os.DirEntry), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) openNew(name string) (osFile, error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("openNew")
|
||||||
|
return expect.Ret.(osFile), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("writeFile").Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "data", data, 1),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) createTemp(dir, pattern string) (osFile, error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("createTemp")
|
||||||
|
return expect.Ret.(osFile), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "dir", dir, 0),
|
||||||
|
stub.CheckArg(k.Stub, "pattern", pattern, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) remove(name string) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("remove").Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) newFile(fd uintptr, name string) *os.File {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("newFile")
|
||||||
|
if expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "fd", fd, 0),
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
return expect.Ret.(*os.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) symlink(oldname, newname string) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("symlink").Error(
|
||||||
|
stub.CheckArg(k.Stub, "oldname", oldname, 0),
|
||||||
|
stub.CheckArg(k.Stub, "newname", newname, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) readlink(name string) (string, error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("readlink")
|
||||||
|
return expect.Ret.(string), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) umask(mask int) (oldmask int) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("umask")
|
||||||
|
if !stub.CheckArg(k.Stub, "mask", mask, 0) {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
return expect.Ret.(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) sethostname(p []byte) (err error) {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("sethostname").Error(
|
||||||
|
stub.CheckArgReflect(k.Stub, "p", p, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) chdir(path string) (err error) {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("chdir").Error(
|
||||||
|
stub.CheckArg(k.Stub, "path", path, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) fchdir(fd int) (err error) {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("fchdir").Error(
|
||||||
|
stub.CheckArg(k.Stub, "fd", fd, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("open")
|
||||||
|
return expect.Ret.(int), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "path", path, 0),
|
||||||
|
stub.CheckArg(k.Stub, "mode", mode, 1),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) close(fd int) (err error) {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("close").Error(
|
||||||
|
stub.CheckArg(k.Stub, "fd", fd, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) pivotRoot(newroot, putold string) (err error) {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("pivotRoot").Error(
|
||||||
|
stub.CheckArg(k.Stub, "newroot", newroot, 0),
|
||||||
|
stub.CheckArg(k.Stub, "putold", putold, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("mount").Error(
|
||||||
|
stub.CheckArg(k.Stub, "source", source, 0),
|
||||||
|
stub.CheckArg(k.Stub, "target", target, 1),
|
||||||
|
stub.CheckArg(k.Stub, "fstype", fstype, 2),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 3),
|
||||||
|
stub.CheckArg(k.Stub, "data", data, 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) unmount(target string, flags int) (err error) {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("unmount").Error(
|
||||||
|
stub.CheckArg(k.Stub, "target", target, 0),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("wait4")
|
||||||
|
if v, ok := expect.Args[4].(int); ok {
|
||||||
|
switch v {
|
||||||
|
case stub.PanicExit: // special case to prevent leaking the wait4 goroutine while testing initEntrypoint
|
||||||
|
panic(stub.PanicExit)
|
||||||
|
|
||||||
|
case magicWait4Signal: // block until corresponding signal call
|
||||||
|
if k.wait4signal == nil {
|
||||||
|
panic("kstub not initialised for wait4 simulation")
|
||||||
|
}
|
||||||
|
<-k.wait4signal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wpid = expect.Ret.(int)
|
||||||
|
err = expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "pid", pid, 0),
|
||||||
|
stub.CheckArg(k.Stub, "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) {
|
||||||
|
k.Helper()
|
||||||
|
if k.Expects("printf").Error(
|
||||||
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) fatal(v ...any) {
|
||||||
|
k.Helper()
|
||||||
|
if k.Expects("fatal").Error(
|
||||||
|
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
panic(stub.PanicExit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) fatalf(format string, v ...any) {
|
||||||
|
k.Helper()
|
||||||
|
if k.Expects("fatalf").Error(
|
||||||
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
panic(stub.PanicExit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) verbose(v ...any) {
|
||||||
|
k.Helper()
|
||||||
|
if k.Expects("verbose").Error(
|
||||||
|
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) verbosef(format string, v ...any) {
|
||||||
|
k.Helper()
|
||||||
|
if k.Expects("verbosef").Error(
|
||||||
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") }
|
||||||
|
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
|
||||||
|
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") }
|
||||||
112
container/errors.go
Normal file
112
container/errors.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"hakurei.app/container/vfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// messageFromError returns a printable error message for a supported concrete type.
|
||||||
|
func messageFromError(err error) (string, bool) {
|
||||||
|
if m, ok := messagePrefixP[MountError]("cannot ", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefixP[AbsoluteError]("", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefix[OpStateError]("", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return zeroString, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// messagePrefix checks and prefixes the error message of a non-pointer error.
|
||||||
|
// While this is usable for pointer errors, such use should be avoided as nil check is omitted.
|
||||||
|
func messagePrefix[T error](prefix string, err error) (string, bool) {
|
||||||
|
var targetError T
|
||||||
|
if errors.As(err, &targetError) {
|
||||||
|
return prefix + targetError.Error(), true
|
||||||
|
}
|
||||||
|
return zeroString, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// messagePrefixP checks and prefixes the error message of a pointer error.
|
||||||
|
func messagePrefixP[V any, T interface {
|
||||||
|
*V
|
||||||
|
error
|
||||||
|
}](prefix string, err error) (string, bool) {
|
||||||
|
var targetError T
|
||||||
|
if errors.As(err, &targetError) && targetError != nil {
|
||||||
|
return prefix + targetError.Error(), true
|
||||||
|
}
|
||||||
|
return zeroString, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type MountError struct {
|
||||||
|
Source, Target, Fstype string
|
||||||
|
|
||||||
|
Flags uintptr
|
||||||
|
Data string
|
||||||
|
syscall.Errno
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MountError) Unwrap() error {
|
||||||
|
if e.Errno == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.Errno
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MountError) Error() string {
|
||||||
|
if e.Flags&syscall.MS_BIND != 0 {
|
||||||
|
if e.Flags&syscall.MS_REMOUNT != 0 {
|
||||||
|
return "remount " + e.Target + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
return "bind " + e.Source + " on " + e.Target + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Fstype != FstypeNULL {
|
||||||
|
return "mount " + e.Fstype + " on " + e.Target + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback case: if this is reached, the conditions for it to occur should be handled above
|
||||||
|
return "mount " + e.Target + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
|
||||||
|
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
||||||
|
var errno syscall.Errno
|
||||||
|
if !errors.As(err, &errno) {
|
||||||
|
return 0, &os.PathError{Op: op, Path: path, Err: err}
|
||||||
|
}
|
||||||
|
return errno, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mount wraps syscall.Mount for error handling.
|
||||||
|
func mount(source, target, fstype string, flags uintptr, data string) error {
|
||||||
|
err := syscall.Mount(source, target, fstype, flags, data)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if errno, pathError := errnoFallback("mount", target, err); pathError != nil {
|
||||||
|
return pathError
|
||||||
|
} else {
|
||||||
|
return &MountError{source, target, fstype, flags, data, errno}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
container/errors_test.go
Normal file
168
container/errors_test.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
"hakurei.app/container/vfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMessageFromError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"mount", &MountError{
|
||||||
|
Source: SourceTmpfsEphemeral,
|
||||||
|
Target: "/sysroot/tmp",
|
||||||
|
Fstype: FstypeTmpfs,
|
||||||
|
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||||
|
Data: zeroString,
|
||||||
|
Errno: syscall.EINVAL,
|
||||||
|
}, "cannot mount tmpfs on /sysroot/tmp: invalid argument", true},
|
||||||
|
|
||||||
|
{"path", &os.PathError{
|
||||||
|
Op: "mount",
|
||||||
|
Path: "/sysroot",
|
||||||
|
Err: stub.UniqueError(0xdeadbeef),
|
||||||
|
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
|
||||||
|
|
||||||
|
{"absolute", &AbsoluteError{"etc/mtab"},
|
||||||
|
`path "etc/mtab" is not absolute`, true},
|
||||||
|
|
||||||
|
{"repeat", OpRepeatError("autoetc"),
|
||||||
|
"autoetc is not repeatable", true},
|
||||||
|
|
||||||
|
{"state", OpStateError("overlay"),
|
||||||
|
"impossible overlay state reached", true},
|
||||||
|
|
||||||
|
{"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
|
||||||
|
`cannot parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, true},
|
||||||
|
|
||||||
|
{"tmpfs", TmpfsSizeError(-1),
|
||||||
|
"tmpfs size -1 out of bounds", true},
|
||||||
|
|
||||||
|
{"unsupported", stub.UniqueError(0xdeadbeef), zeroString, false},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, ok := messageFromError(tc.err)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("messageFromError: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
if ok != tc.wantOk {
|
||||||
|
t.Errorf("messageFromError: ok = %v, want %v", ok, tc.wantOk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
errno syscall.Errno
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"bind", &MountError{
|
||||||
|
Source: "/host/nix/store",
|
||||||
|
Target: "/sysroot/nix/store",
|
||||||
|
Fstype: FstypeNULL,
|
||||||
|
Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REC,
|
||||||
|
Data: zeroString,
|
||||||
|
Errno: syscall.ENOSYS,
|
||||||
|
}, syscall.ENOSYS,
|
||||||
|
"bind /host/nix/store on /sysroot/nix/store: function not implemented"},
|
||||||
|
|
||||||
|
{"remount", &MountError{
|
||||||
|
Source: SourceNone,
|
||||||
|
Target: "/sysroot/nix/store",
|
||||||
|
Fstype: FstypeNULL,
|
||||||
|
Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REMOUNT,
|
||||||
|
Data: zeroString,
|
||||||
|
Errno: syscall.EPERM,
|
||||||
|
}, syscall.EPERM,
|
||||||
|
"remount /sysroot/nix/store: operation not permitted"},
|
||||||
|
|
||||||
|
{"overlay", &MountError{
|
||||||
|
Source: SourceOverlay,
|
||||||
|
Target: sysrootPath,
|
||||||
|
Fstype: FstypeOverlay,
|
||||||
|
Data: `lowerdir=/host/var/lib/planterette/base/debian\:f92c9052`,
|
||||||
|
Errno: syscall.EINVAL,
|
||||||
|
}, syscall.EINVAL,
|
||||||
|
"mount overlay on /sysroot: invalid argument"},
|
||||||
|
|
||||||
|
{"fallback", &MountError{
|
||||||
|
Source: SourceNone,
|
||||||
|
Target: sysrootPath,
|
||||||
|
Fstype: FstypeNULL,
|
||||||
|
Errno: syscall.ENOTRECOVERABLE,
|
||||||
|
}, syscall.ENOTRECOVERABLE,
|
||||||
|
"mount /sysroot: state not recoverable"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
if !errors.Is(tc.err, tc.errno) {
|
||||||
|
t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("error", func(t *testing.T) {
|
||||||
|
if got := tc.err.Error(); got != tc.want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("zero", func(t *testing.T) {
|
||||||
|
if errors.Is(new(MountError), syscall.Errno(0)) {
|
||||||
|
t.Errorf("Is: zero MountError unexpected true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrnoFallback(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
wantErrno syscall.Errno
|
||||||
|
wantPath *os.PathError
|
||||||
|
}{
|
||||||
|
{"mount", &MountError{
|
||||||
|
Errno: syscall.ENOTRECOVERABLE,
|
||||||
|
}, syscall.ENOTRECOVERABLE, nil},
|
||||||
|
|
||||||
|
{"path errno", &os.PathError{
|
||||||
|
Err: syscall.ETIMEDOUT,
|
||||||
|
}, syscall.ETIMEDOUT, nil},
|
||||||
|
|
||||||
|
{"fallback", stub.UniqueError(0xcafebabe), 0, &os.PathError{
|
||||||
|
Op: "fallback",
|
||||||
|
Path: "/proc/nonexistent",
|
||||||
|
Err: stub.UniqueError(0xcafebabe),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
|
||||||
|
if errno != tc.wantErrno {
|
||||||
|
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(err, tc.wantPath) {
|
||||||
|
t.Errorf("errnoFallback: pathError = %#v, want %#v", err, tc.wantPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalMessageFromError exports messageFromError for other tests.
|
||||||
|
func InternalMessageFromError(err error) (string, bool) { return messageFromError(err) }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package sandbox
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package sandbox_test
|
package container_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"hakurei.app/container"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExecutable(t *testing.T) {
|
func TestExecutable(t *testing.T) {
|
||||||
for i := 0; i < 16; i++ {
|
for i := 0; i < 16; i++ {
|
||||||
if got := sandbox.MustExecutable(); got != os.Args[0] {
|
if got := container.MustExecutable(); got != os.Args[0] {
|
||||||
t.Errorf("MustExecutable: %q, want %q",
|
t.Errorf("MustExecutable: %q, want %q",
|
||||||
got, os.Args[0])
|
got, os.Args[0])
|
||||||
}
|
}
|
||||||
451
container/init.go
Normal file
451
container/init.go
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
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 returns a log message prefix, and whether this Op prints no identifying message on its own.
|
||||||
|
prefix() (string, bool)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpRepeatError is returned applying a repeated nonrepeatable [Op].
|
||||||
|
type OpRepeatError string
|
||||||
|
|
||||||
|
func (e OpRepeatError) Error() string { return string(e) + " is not repeatable" }
|
||||||
|
|
||||||
|
// OpStateError indicates an impossible internal state has been reached in an [Op].
|
||||||
|
type OpStateError string
|
||||||
|
|
||||||
|
func (o OpStateError) Error() string { return "impossible " + string(o) + " state reached" }
|
||||||
|
|
||||||
|
// 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, ErrReceiveEnv) {
|
||||||
|
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 {
|
||||||
|
if m, ok := messageFromError(err); ok {
|
||||||
|
k.fatal(m)
|
||||||
|
} else {
|
||||||
|
k.fatalf("cannot prepare op at index %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if prefix, ok := op.prefix(); ok {
|
||||||
|
k.verbosef("%s %s", prefix, op)
|
||||||
|
}
|
||||||
|
if err := op.apply(state, k); err != nil {
|
||||||
|
if m, ok := messageFromError(err); ok {
|
||||||
|
k.fatal(m)
|
||||||
|
} else {
|
||||||
|
k.fatalf("cannot apply op at index %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
2630
container/init_test.go
Normal file
2630
container/init_test.go
Normal file
File diff suppressed because it is too large
Load Diff
118
container/initbind.go
Normal file
118
container/initbind.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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 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 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 OpStateError("bind")
|
||||||
|
}
|
||||||
|
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 err
|
||||||
|
} else if fi.IsDir() {
|
||||||
|
if err = k.mkdirAll(target, 0700); err != nil {
|
||||||
|
return 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.sourceFinal.String() == b.Target.String() {
|
||||||
|
k.verbosef("mounting %q flags %#x", target, flags)
|
||||||
|
} else {
|
||||||
|
k.verbosef("mounting %q on %q flags %#x", source, target, flags)
|
||||||
|
}
|
||||||
|
return k.bindMount(source, target, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, bool) { return "mounting", false }
|
||||||
|
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)
|
||||||
|
}
|
||||||
255
container/initbind_test.go
Normal file
255
container/initbind_test.go
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBindMountOp(t *testing.T) {
|
||||||
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
|
{"ENOENT not optional", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
|
||||||
|
}, syscall.ENOENT, nil, nil},
|
||||||
|
|
||||||
|
{"skip optional", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
Flags: BindOptional,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
|
||||||
|
}, nil, nil, nil},
|
||||||
|
|
||||||
|
{"success optional", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
Flags: BindOptional,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
||||||
|
call("bindMount", stub.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,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(5)),
|
||||||
|
}, stub.UniqueError(5)},
|
||||||
|
|
||||||
|
{"mkdirAll ensure", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
Flags: BindEnsure,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
|
||||||
|
}, stub.UniqueError(4), nil, nil},
|
||||||
|
|
||||||
|
{"success ensure", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/usr/bin/"),
|
||||||
|
Flags: BindEnsure,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005)}}, nil, nil),
|
||||||
|
call("bindMount", stub.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,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/dev/null", uintptr(0x4001)}}, nil, nil),
|
||||||
|
call("bindMount", stub.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,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/dev/null", uintptr(0x4000)}}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil),
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"evalSymlinks", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
|
||||||
|
}, stub.UniqueError(3), nil, nil},
|
||||||
|
|
||||||
|
{"stat", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), stub.UniqueError(2)),
|
||||||
|
}, stub.UniqueError(2)},
|
||||||
|
|
||||||
|
{"mkdirAll", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, stub.UniqueError(1)),
|
||||||
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
|
{"bindMount", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)),
|
||||||
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
|
{"success eval equals", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/bin", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/bin"}, isDirFi(true), nil),
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"success", new(Params), &BindMountOp{
|
||||||
|
Source: MustAbs("/bin/"),
|
||||||
|
Target: MustAbs("/bin/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
||||||
|
call("bindMount", stub.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 := OpStateError("bind")
|
||||||
|
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`},
|
||||||
|
})
|
||||||
|
}
|
||||||
138
container/initdev.go
Normal file
138
container/initdev.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
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,
|
||||||
|
); 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 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 err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devShmPath := path.Join(target, "shm")
|
||||||
|
devPtsPath := path.Join(target, "pts")
|
||||||
|
for _, name := range []string{devShmPath, devPtsPath} {
|
||||||
|
if err := k.mkdir(name, state.ParentPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
|
||||||
|
"newinstance,ptmxmode=0666,mode=620"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 err
|
||||||
|
} else if err = k.bindMount(
|
||||||
|
toHost(name),
|
||||||
|
consolePath,
|
||||||
|
0,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Mqueue {
|
||||||
|
mqueueTarget := path.Join(target, "mqueue")
|
||||||
|
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Write {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := k.remount(target, MS_RDONLY); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, bool) { return "mounting", true }
|
||||||
|
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)
|
||||||
|
}
|
||||||
825
container/initdev_test.go
Normal file
825
container/initdev_test.go
Normal file
@@ -0,0 +1,825 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMountDevOp(t *testing.T) {
|
||||||
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
|
{"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, stub.UniqueError(27)),
|
||||||
|
}, stub.UniqueError(27)},
|
||||||
|
|
||||||
|
{"ensureFile null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, stub.UniqueError(26)),
|
||||||
|
}, stub.UniqueError(26)},
|
||||||
|
|
||||||
|
{"bindMount null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, stub.UniqueError(25)),
|
||||||
|
}, stub.UniqueError(25)},
|
||||||
|
|
||||||
|
{"ensureFile zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, stub.UniqueError(24)),
|
||||||
|
}, stub.UniqueError(24)},
|
||||||
|
|
||||||
|
{"bindMount zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, stub.UniqueError(23)),
|
||||||
|
}, stub.UniqueError(23)},
|
||||||
|
|
||||||
|
{"ensureFile full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, stub.UniqueError(22)),
|
||||||
|
}, stub.UniqueError(22)},
|
||||||
|
|
||||||
|
{"bindMount full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, stub.UniqueError(21)),
|
||||||
|
}, stub.UniqueError(21)},
|
||||||
|
|
||||||
|
{"ensureFile random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, stub.UniqueError(20)),
|
||||||
|
}, stub.UniqueError(20)},
|
||||||
|
|
||||||
|
{"bindMount random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, stub.UniqueError(19)),
|
||||||
|
}, stub.UniqueError(19)},
|
||||||
|
|
||||||
|
{"ensureFile urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, stub.UniqueError(18)),
|
||||||
|
}, stub.UniqueError(18)},
|
||||||
|
|
||||||
|
{"bindMount urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, stub.UniqueError(17)),
|
||||||
|
}, stub.UniqueError(17)},
|
||||||
|
|
||||||
|
{"ensureFile tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, stub.UniqueError(16)),
|
||||||
|
}, stub.UniqueError(16)},
|
||||||
|
|
||||||
|
{"bindMount tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, stub.UniqueError(15)),
|
||||||
|
}, stub.UniqueError(15)},
|
||||||
|
|
||||||
|
{"symlink stdin", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, stub.UniqueError(14)),
|
||||||
|
}, stub.UniqueError(14)},
|
||||||
|
|
||||||
|
{"symlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, stub.UniqueError(13)),
|
||||||
|
}, stub.UniqueError(13)},
|
||||||
|
|
||||||
|
{"symlink stderr", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, stub.UniqueError(12)),
|
||||||
|
}, stub.UniqueError(12)},
|
||||||
|
|
||||||
|
{"symlink fd", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, stub.UniqueError(11)),
|
||||||
|
}, stub.UniqueError(11)},
|
||||||
|
|
||||||
|
{"symlink kcore", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, stub.UniqueError(10)),
|
||||||
|
}, stub.UniqueError(10)},
|
||||||
|
|
||||||
|
{"symlink ptmx", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, stub.UniqueError(9)),
|
||||||
|
}, stub.UniqueError(9)},
|
||||||
|
|
||||||
|
{"mkdir shm", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, stub.UniqueError(8)),
|
||||||
|
}, stub.UniqueError(8)},
|
||||||
|
|
||||||
|
{"mkdir devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, stub.UniqueError(7)),
|
||||||
|
}, stub.UniqueError(7)},
|
||||||
|
|
||||||
|
{"mount devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, stub.UniqueError(6)),
|
||||||
|
}, stub.UniqueError(6)},
|
||||||
|
|
||||||
|
{"ensureFile stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, stub.UniqueError(5)),
|
||||||
|
}, stub.UniqueError(5)},
|
||||||
|
|
||||||
|
{"readlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/1"}, "", stub.UniqueError(4)),
|
||||||
|
}, stub.UniqueError(4)},
|
||||||
|
|
||||||
|
{"bindMount stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, stub.UniqueError(3)),
|
||||||
|
}, stub.UniqueError(3)},
|
||||||
|
|
||||||
|
{"mkdir mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, stub.UniqueError(2)),
|
||||||
|
}, stub.UniqueError(2)},
|
||||||
|
|
||||||
|
{"mount mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, stub.UniqueError(1)),
|
||||||
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
|
{"success no session", &Params{ParentPerm: 0755}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
Write: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0755)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0755)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0755)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0755)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0755)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0755)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0755)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/mqueue", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("mount", stub.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, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, false, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil),
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"remount", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil),
|
||||||
|
call("remount", stub.ExpectArgs{"/sysroot/dev", uintptr(1)}, nil, stub.UniqueError(0)),
|
||||||
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
|
{"success no mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil),
|
||||||
|
call("remount", stub.ExpectArgs{"/sysroot/dev", uintptr(1)}, nil, nil),
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"tmpfs", "/sysroot/dev/shm", uintptr(0x6), 0, os.FileMode(01777)}, nil, nil),
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"success rw", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
|
||||||
|
Target: MustAbs("/dev/"),
|
||||||
|
Mqueue: true,
|
||||||
|
Write: true,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.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, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil),
|
||||||
|
call("isatty", stub.ExpectArgs{1}, true, nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil),
|
||||||
|
call("mkdir", stub.ExpectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil),
|
||||||
|
call("remount", stub.ExpectArgs{"/sysroot/dev", uintptr(1)}, nil, nil),
|
||||||
|
call("mountTmpfs", stub.ExpectArgs{"tmpfs", "/sysroot/dev/shm", uintptr(0x6), 0, os.FileMode(01777)}, 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 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, bool) { return "creating", true }
|
||||||
|
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
||||||
44
container/initmkdir_test.go
Normal file
44
container/initmkdir_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMkdirOp(t *testing.T) {
|
||||||
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
|
{"success", new(Params), &MkdirOp{
|
||||||
|
Path: MustAbs("/.hakurei"),
|
||||||
|
Perm: 0500,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.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 ----------`},
|
||||||
|
})
|
||||||
|
}
|
||||||
215
container/initoverlay.go
Normal file
215
container/initoverlay.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"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)) }
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OverlayEphemeralUnexpectedUpper is set when [MountOverlayOp.Work] is nil
|
||||||
|
// and [MountOverlayOp.Upper] holds an unexpected value.
|
||||||
|
OverlayEphemeralUnexpectedUpper = iota
|
||||||
|
// OverlayReadonlyLower is set when [MountOverlayOp.Lower] contains less than
|
||||||
|
// two entries when mounting readonly.
|
||||||
|
OverlayReadonlyLower
|
||||||
|
// OverlayEmptyLower is set when [MountOverlayOp.Lower] has length of zero.
|
||||||
|
OverlayEmptyLower
|
||||||
|
)
|
||||||
|
|
||||||
|
// OverlayArgumentError is returned for [MountOverlayOp] supplied with invalid argument.
|
||||||
|
type OverlayArgumentError struct {
|
||||||
|
Type uintptr
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *OverlayArgumentError) Error() string {
|
||||||
|
switch e.Type {
|
||||||
|
case OverlayEphemeralUnexpectedUpper:
|
||||||
|
return fmt.Sprintf("upperdir has unexpected value %q", e.Value)
|
||||||
|
|
||||||
|
case OverlayReadonlyLower:
|
||||||
|
return "readonly overlay requires at least two lowerdir"
|
||||||
|
|
||||||
|
case OverlayEmptyLower:
|
||||||
|
return "overlay requires at least one lowerdir"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("invalid overlay argument error %#x", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, o.Upper.String()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// readonly handled in apply
|
||||||
|
|
||||||
|
if !o.ephemeral {
|
||||||
|
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
|
||||||
|
// unreachable
|
||||||
|
return OpStateError("overlay")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Upper != nil {
|
||||||
|
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
o.upper = EscapeOverlayDataSegment(toHost(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Work != nil {
|
||||||
|
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
|
||||||
|
return 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 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 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 err
|
||||||
|
}
|
||||||
|
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make([]string, 0, 4)
|
||||||
|
|
||||||
|
if o.upper == zeroString && o.work == zeroString { // readonly
|
||||||
|
if len(o.Lower) < 2 {
|
||||||
|
return &OverlayArgumentError{OverlayReadonlyLower, zeroString}
|
||||||
|
}
|
||||||
|
// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
|
||||||
|
} else {
|
||||||
|
if len(o.Lower) == 0 {
|
||||||
|
return &OverlayArgumentError{OverlayEmptyLower, zeroString}
|
||||||
|
}
|
||||||
|
options = append(options,
|
||||||
|
OptionOverlayUpperdir+"="+o.upper,
|
||||||
|
OptionOverlayWorkdir+"="+o.work)
|
||||||
|
}
|
||||||
|
options = append(options,
|
||||||
|
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
|
||||||
|
OptionOverlayUserxattr)
|
||||||
|
|
||||||
|
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption))
|
||||||
|
}
|
||||||
|
|
||||||
|
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, bool) { return "mounting", true }
|
||||||
|
func (o *MountOverlayOp) String() string {
|
||||||
|
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
|
||||||
|
}
|
||||||
396
container/initoverlay_test.go
Normal file
396
container/initoverlay_test.go
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMountOverlayOp(t *testing.T) {
|
||||||
|
t.Run("argument error", func(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err *OverlayArgumentError
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"unexpected upper", &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"},
|
||||||
|
`upperdir has unexpected value "/proc/"`},
|
||||||
|
|
||||||
|
{"lower ro short", &OverlayArgumentError{OverlayReadonlyLower, zeroString},
|
||||||
|
"readonly overlay requires at least two lowerdir"},
|
||||||
|
|
||||||
|
{"lower short", &OverlayArgumentError{OverlayEmptyLower, zeroString},
|
||||||
|
"overlay requires at least one lowerdir"},
|
||||||
|
|
||||||
|
{"oob", &OverlayArgumentError{0xdeadbeef, zeroString},
|
||||||
|
"invalid overlay argument error 0xdeadbeef"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := tc.err.Error(); got != tc.want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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, &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/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("/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
||||||
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", stub.UniqueError(6)),
|
||||||
|
}, stub.UniqueError(6)},
|
||||||
|
|
||||||
|
{"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("/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
||||||
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
|
||||||
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", stub.UniqueError(5)),
|
||||||
|
}, stub.UniqueError(5)},
|
||||||
|
|
||||||
|
{"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("/"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
||||||
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
|
||||||
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
|
||||||
|
call("mount", stub.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"),
|
||||||
|
},
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
|
||||||
|
}, &OverlayArgumentError{OverlayReadonlyLower, zeroString}},
|
||||||
|
|
||||||
|
{"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,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("mount", stub.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"),
|
||||||
|
},
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("mount", stub.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"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
|
}, &OverlayArgumentError{OverlayEmptyLower, zeroString}},
|
||||||
|
|
||||||
|
{"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"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", stub.UniqueError(4)),
|
||||||
|
}, stub.UniqueError(4), 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"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", stub.UniqueError(3)),
|
||||||
|
}, stub.UniqueError(3), 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"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", stub.UniqueError(2)),
|
||||||
|
}, stub.UniqueError(2), 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"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, stub.UniqueError(1)),
|
||||||
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
|
{"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"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("mount", stub.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, stub.UniqueError(0)),
|
||||||
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
|
{"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"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("mount", stub.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"),
|
||||||
|
}, []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil),
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("mount", stub.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 := OpStateError("overlay")
|
||||||
|
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`},
|
||||||
|
})
|
||||||
|
}
|
||||||
75
container/initplace.go
Normal file
75
container/initplace.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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 err
|
||||||
|
} else if _, err = f.Write(t.Data); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
} 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,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = k.remove(tmpPath); err != nil {
|
||||||
|
return 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, bool) { return "placing", true }
|
||||||
|
func (t *TmpfileOp) String() string {
|
||||||
|
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
|
||||||
|
}
|
||||||
133
container/initplace_test.go
Normal file
133
container/initplace_test.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, []stub.Call{
|
||||||
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), stub.UniqueError(5)),
|
||||||
|
}, stub.UniqueError(5)},
|
||||||
|
|
||||||
|
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
|
Path: samplePath,
|
||||||
|
Data: sampleData,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, writeErrOsFile{stub.UniqueError(4)}, nil),
|
||||||
|
}, stub.UniqueError(4)},
|
||||||
|
|
||||||
|
{"Close", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
|
Path: samplePath,
|
||||||
|
Data: sampleData,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, stub.UniqueError(3)), nil),
|
||||||
|
}, stub.UniqueError(3)},
|
||||||
|
|
||||||
|
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
|
Path: samplePath,
|
||||||
|
Data: sampleData,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(2)),
|
||||||
|
}, stub.UniqueError(2)},
|
||||||
|
|
||||||
|
{"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
|
Path: samplePath,
|
||||||
|
Data: sampleData,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, stub.UniqueError(1)),
|
||||||
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
|
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
|
Path: samplePath,
|
||||||
|
Data: sampleData,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
|
||||||
|
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, stub.UniqueError(0)),
|
||||||
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
|
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
|
Path: samplePath,
|
||||||
|
Data: sampleData,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
|
||||||
|
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
|
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
|
||||||
|
call("remove", stub.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)`},
|
||||||
|
})
|
||||||
|
}
|
||||||
36
container/initproc.go
Normal file
36
container/initproc.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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 err
|
||||||
|
}
|
||||||
|
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, bool) { return "mounting", true }
|
||||||
|
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
|
||||||
60
container/initproc_test.go
Normal file
60
container/initproc_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMountProcOp(t *testing.T) {
|
||||||
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
|
{"mkdir", &Params{ParentPerm: 0755},
|
||||||
|
&MountProcOp{
|
||||||
|
Target: MustAbs("/proc/"),
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, stub.UniqueError(0)),
|
||||||
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
|
{"success", &Params{ParentPerm: 0700},
|
||||||
|
&MountProcOp{
|
||||||
|
Target: MustAbs("/proc/"),
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("mount", stub.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/"`},
|
||||||
|
})
|
||||||
|
}
|
||||||
35
container/initremount.go
Normal file
35
container/initremount.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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 k.remount(toSysroot(r.Target.String()), r.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, bool) { return "remounting", true }
|
||||||
|
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
|
||||||
69
container/initremount_test.go
Normal file
69
container/initremount_test.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemountOp(t *testing.T) {
|
||||||
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
|
{"success", new(Params), &RemountOp{
|
||||||
|
Target: MustAbs("/"),
|
||||||
|
Flags: syscall.MS_RDONLY,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("remount", stub.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`},
|
||||||
|
})
|
||||||
|
}
|
||||||
61
container/initsymlink.go
Normal file
61
container/initsymlink.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"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 &AbsoluteError{l.LinkName}
|
||||||
|
}
|
||||||
|
if name, err := k.readlink(l.LinkName); err != nil {
|
||||||
|
return 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 err
|
||||||
|
}
|
||||||
|
return 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, bool) { return "creating", true }
|
||||||
|
func (l *SymlinkOp) String() string {
|
||||||
|
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
|
||||||
|
}
|
||||||
125
container/initsymlink_test.go
Normal file
125
container/initsymlink_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSymlinkOp(t *testing.T) {
|
||||||
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
|
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
|
||||||
|
Target: MustAbs("/etc/nixos"),
|
||||||
|
LinkName: "/etc/static/nixos",
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, stub.UniqueError(1)),
|
||||||
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
|
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||||
|
Target: MustAbs("/etc/mtab"),
|
||||||
|
LinkName: "etc/mtab",
|
||||||
|
Dereference: true,
|
||||||
|
}, nil, &AbsoluteError{"etc/mtab"}, nil, nil},
|
||||||
|
|
||||||
|
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||||
|
Target: MustAbs("/etc/mtab"),
|
||||||
|
LinkName: "/etc/mtab",
|
||||||
|
Dereference: true,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", stub.UniqueError(0)),
|
||||||
|
}, stub.UniqueError(0), nil, nil},
|
||||||
|
|
||||||
|
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
|
||||||
|
Target: MustAbs("/etc/nixos"),
|
||||||
|
LinkName: "/etc/static/nixos",
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("symlink", stub.ExpectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil),
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||||
|
Target: MustAbs("/etc/mtab"),
|
||||||
|
LinkName: "/etc/mtab",
|
||||||
|
Dereference: true,
|
||||||
|
}, []stub.Call{
|
||||||
|
call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", nil),
|
||||||
|
}, nil, []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil),
|
||||||
|
call("symlink", stub.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"`},
|
||||||
|
})
|
||||||
|
}
|
||||||
60
container/inittmpfs.go
Normal file
60
container/inittmpfs.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
. "syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { gob.Register(new(MountTmpfsOp)) }
|
||||||
|
|
||||||
|
type TmpfsSizeError int
|
||||||
|
|
||||||
|
func (e TmpfsSizeError) Error() string {
|
||||||
|
return "tmpfs size " + strconv.Itoa(int(e)) + " out of bounds"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 TmpfsSizeError(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, bool) { return "mounting", true }
|
||||||
|
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||||
174
container/inittmpfs_test.go
Normal file
174
container/inittmpfs_test.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMountTmpfsOp(t *testing.T) {
|
||||||
|
t.Run("size error", func(t *testing.T) {
|
||||||
|
tmpfsSizeError := TmpfsSizeError(-1)
|
||||||
|
want := "tmpfs size -1 out of bounds"
|
||||||
|
if got := tmpfsSizeError.Error(); got != want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
|
{"size oob", new(Params), &MountTmpfsOp{
|
||||||
|
Size: -1,
|
||||||
|
}, nil, nil, nil, TmpfsSizeError(-1)},
|
||||||
|
|
||||||
|
{"success", new(Params), &MountTmpfsOp{
|
||||||
|
FSName: "ephemeral",
|
||||||
|
Path: MustAbs("/run/user/1000/"),
|
||||||
|
Size: 1 << 10,
|
||||||
|
Perm: 0700,
|
||||||
|
}, nil, nil, []stub.Call{
|
||||||
|
call("mountTmpfs", stub.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
227
container/mount.go
Normal file
227
container/mount.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
// SourceTmpfs is used when mounting tmpfs.
|
||||||
|
SourceTmpfs = "tmpfs"
|
||||||
|
// 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) error {
|
||||||
|
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
|
||||||
|
|
||||||
|
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 &os.PathError{Op: "open", Path: targetFinal, Err: err}
|
||||||
|
}
|
||||||
|
if v, err := p.k.readlink(p.fd(destFd)); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = p.k.close(destFd); err != nil {
|
||||||
|
return &os.PathError{Op: "close", Path: targetFinal, Err: err}
|
||||||
|
} 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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = remountWithFlags(p.k, n, mf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if flags&MS_REC == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for cur := range n.Collective() {
|
||||||
|
// avoid remounting twice
|
||||||
|
if cur == n {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = remountWithFlags(p.k, cur, mf); err != nil && !errors.Is(err, EACCES) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 err
|
||||||
|
}
|
||||||
|
opt := fmt.Sprintf("mode=%#o", perm)
|
||||||
|
if size > 0 {
|
||||||
|
opt += fmt.Sprintf(",size=%d", size)
|
||||||
|
}
|
||||||
|
return k.mount(fsname, target, FstypeTmpfs, flags, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
303
container/mount_test.go
Normal file
303
container/mount_test.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
"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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
|
||||||
|
}}, stub.UniqueError(0xbad)},
|
||||||
|
|
||||||
|
{"success ne", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil),
|
||||||
|
call("remount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
|
||||||
|
call("remount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)),
|
||||||
|
}}, stub.UniqueError(6)},
|
||||||
|
|
||||||
|
{"open", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)),
|
||||||
|
}}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}},
|
||||||
|
|
||||||
|
{"readlink", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)),
|
||||||
|
}}, stub.UniqueError(4)},
|
||||||
|
|
||||||
|
{"close", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)),
|
||||||
|
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}},
|
||||||
|
|
||||||
|
{"mountinfo no match", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
|
}}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}},
|
||||||
|
|
||||||
|
{"mountinfo", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil),
|
||||||
|
}}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}},
|
||||||
|
|
||||||
|
{"mount", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)),
|
||||||
|
}}, stub.UniqueError(2)},
|
||||||
|
|
||||||
|
{"mount propagate", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)),
|
||||||
|
}}, stub.UniqueError(1)},
|
||||||
|
|
||||||
|
{"success toplevel", func(k syscallDispatcher) error {
|
||||||
|
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xbabe}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
|
call("mount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES),
|
||||||
|
call("mount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
|
call("mount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
|
call("mount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil),
|
||||||
|
call("open", stub.ExpectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
|
call("mount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("verbosef", stub.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)
|
||||||
|
}, stub.Expect{}, nil},
|
||||||
|
|
||||||
|
{"success", func(k syscallDispatcher) error {
|
||||||
|
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("mount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)),
|
||||||
|
}}, stub.UniqueError(0)},
|
||||||
|
|
||||||
|
{"success no size", func(k syscallDispatcher) error {
|
||||||
|
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil),
|
||||||
|
call("mount", stub.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)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil),
|
||||||
|
call("mount", stub.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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
67
container/msg.go
Normal file
67
container/msg.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MessageError is an error with a user-facing message.
|
||||||
|
type MessageError interface {
|
||||||
|
// Message returns a user-facing error message.
|
||||||
|
Message() string
|
||||||
|
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrorMessage returns whether an error implements [MessageError], and the message if it does.
|
||||||
|
func GetErrorMessage(err error) (string, bool) {
|
||||||
|
var e MessageError
|
||||||
|
if !errors.As(err, &e) || e == nil {
|
||||||
|
return zeroString, false
|
||||||
|
}
|
||||||
|
return e.Message(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
type Msg interface {
|
||||||
|
IsVerbose() bool
|
||||||
|
Verbose(v ...any)
|
||||||
|
Verbosef(format string, v ...any)
|
||||||
|
|
||||||
|
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...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
|
||||||
|
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
|
||||||
|
func (msg *DefaultMsg) BeforeExit() {}
|
||||||
|
|
||||||
|
// msg is the [Msg] implemented used by all exported [container] functions.
|
||||||
|
var msg Msg = new(DefaultMsg)
|
||||||
|
|
||||||
|
// GetOutput returns the current active [Msg] implementation.
|
||||||
|
func GetOutput() Msg { return msg }
|
||||||
|
|
||||||
|
// SetOutput replaces the current active [Msg] implementation.
|
||||||
|
func SetOutput(v Msg) {
|
||||||
|
if v == nil {
|
||||||
|
msg = new(DefaultMsg)
|
||||||
|
} else {
|
||||||
|
msg = v
|
||||||
|
}
|
||||||
|
}
|
||||||
184
container/msg_test.go
Normal file
184
container/msg_test.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package container_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMessageError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"nil", nil, "", false},
|
||||||
|
{"new", errors.New(":3"), "", false},
|
||||||
|
{"start", &container.StartError{
|
||||||
|
Step: "meow",
|
||||||
|
Err: syscall.ENOTRECOVERABLE,
|
||||||
|
}, "cannot meow: state not recoverable", true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, ok := container.GetErrorMessage(tc.err)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("GetErrorMessage: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
if ok != tc.wantOk {
|
||||||
|
t.Errorf("GetErrorMessage: ok = %v, want %v", ok, tc.wantOk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultMsg(t *testing.T) {
|
||||||
|
{
|
||||||
|
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("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() })
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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") }
|
||||||
|
|
||||||
|
func TestGetSetOutput(t *testing.T) {
|
||||||
|
{
|
||||||
|
out := container.GetOutput()
|
||||||
|
t.Cleanup(func() { container.SetOutput(out) })
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("default", func(t *testing.T) {
|
||||||
|
container.SetOutput(new(stubOutput))
|
||||||
|
if v, ok := container.GetOutput().(*container.DefaultMsg); ok {
|
||||||
|
t.Fatalf("SetOutput: got unexpected output %#v", v)
|
||||||
|
}
|
||||||
|
container.SetOutput(nil)
|
||||||
|
if _, ok := container.GetOutput().(*container.DefaultMsg); !ok {
|
||||||
|
t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stub", func(t *testing.T) {
|
||||||
|
container.SetOutput(new(stubOutput))
|
||||||
|
if _, ok := container.GetOutput().(*stubOutput); !ok {
|
||||||
|
t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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) Suspend() { panic("unreachable") }
|
||||||
|
func (*stubOutput) Resume() bool { panic("unreachable") }
|
||||||
|
func (*stubOutput) BeforeExit() { panic("unreachable") }
|
||||||
77
container/output.go
Normal file
77
container/output.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
suspendBufInitial = 1 << 12
|
||||||
|
suspendBufMax = 1 << 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// Suspendable proxies writes to a downstream [io.Writer] but optionally withholds writes
|
||||||
|
// between calls to Suspend and Resume.
|
||||||
|
type Suspendable struct {
|
||||||
|
Downstream io.Writer
|
||||||
|
|
||||||
|
s atomic.Bool
|
||||||
|
|
||||||
|
buf bytes.Buffer
|
||||||
|
// for growing buf
|
||||||
|
bufOnce sync.Once
|
||||||
|
// for synchronising all other buf operations
|
||||||
|
bufMu sync.Mutex
|
||||||
|
|
||||||
|
dropped int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suspendable) Write(p []byte) (n int, err error) {
|
||||||
|
if !s.s.Load() {
|
||||||
|
return s.Downstream.Write(p)
|
||||||
|
}
|
||||||
|
s.bufOnce.Do(func() { s.buf.Grow(suspendBufInitial) })
|
||||||
|
|
||||||
|
s.bufMu.Lock()
|
||||||
|
defer s.bufMu.Unlock()
|
||||||
|
|
||||||
|
if free := suspendBufMax - s.buf.Len(); free < len(p) {
|
||||||
|
// fast path
|
||||||
|
if free <= 0 {
|
||||||
|
s.dropped += len(p)
|
||||||
|
return 0, syscall.ENOMEM
|
||||||
|
}
|
||||||
|
|
||||||
|
n, _ = s.buf.Write(p[:free])
|
||||||
|
err = syscall.ENOMEM
|
||||||
|
s.dropped += len(p) - n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.buf.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSuspended returns whether [Suspendable] is currently between a call to Suspend and Resume.
|
||||||
|
func (s *Suspendable) IsSuspended() bool { return s.s.Load() }
|
||||||
|
|
||||||
|
// Suspend causes [Suspendable] to start withholding output in its buffer.
|
||||||
|
func (s *Suspendable) Suspend() bool { return s.s.CompareAndSwap(false, true) }
|
||||||
|
|
||||||
|
// Resume undoes the effect of Suspend and dumps the buffered into the downstream [io.Writer].
|
||||||
|
func (s *Suspendable) Resume() (resumed bool, dropped uintptr, n int64, err error) {
|
||||||
|
if s.s.CompareAndSwap(true, false) {
|
||||||
|
s.bufMu.Lock()
|
||||||
|
defer s.bufMu.Unlock()
|
||||||
|
|
||||||
|
resumed = true
|
||||||
|
dropped = uintptr(s.dropped)
|
||||||
|
|
||||||
|
s.dropped = 0
|
||||||
|
n, err = io.Copy(s.Downstream, &s.buf)
|
||||||
|
s.buf.Reset()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
155
container/output_test.go
Normal file
155
container/output_test.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package container_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSuspendable(t *testing.T) {
|
||||||
|
// copied from output.go
|
||||||
|
const suspendBufMax = 1 << 24
|
||||||
|
|
||||||
|
const (
|
||||||
|
// equivalent to len(want.pt)
|
||||||
|
nSpecialPtEquiv = -iota - 1
|
||||||
|
// equivalent to len(want.w)
|
||||||
|
nSpecialWEquiv
|
||||||
|
// suspends writer before executing test case, implies nSpecialWEquiv
|
||||||
|
nSpecialSuspend
|
||||||
|
// offset: resume writer and measure against dump instead, implies nSpecialPtEquiv
|
||||||
|
nSpecialDump
|
||||||
|
)
|
||||||
|
|
||||||
|
// shares the same writer
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
w, pt []byte
|
||||||
|
err error
|
||||||
|
wantErr error
|
||||||
|
n int
|
||||||
|
}{
|
||||||
|
{"simple", []byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xde, 0xad, 0xbe, 0xef},
|
||||||
|
nil, nil, nSpecialPtEquiv},
|
||||||
|
|
||||||
|
{"error", []byte{0xb, 0xad}, []byte{0xb, 0xad},
|
||||||
|
stub.UniqueError(0), stub.UniqueError(0), nSpecialPtEquiv},
|
||||||
|
|
||||||
|
{"suspend short", []byte{0}, nil,
|
||||||
|
nil, nil, nSpecialSuspend},
|
||||||
|
{"sw short 0", []byte{0xca, 0xfe, 0xba, 0xbe}, nil,
|
||||||
|
nil, nil, nSpecialWEquiv},
|
||||||
|
{"sw short 1", []byte{0xff}, nil,
|
||||||
|
nil, nil, nSpecialWEquiv},
|
||||||
|
{"resume short", nil, []byte{0, 0xca, 0xfe, 0xba, 0xbe, 0xff}, nil, nil,
|
||||||
|
nSpecialDump},
|
||||||
|
|
||||||
|
{"long pt", bytes.Repeat([]byte{0xff}, suspendBufMax+1), bytes.Repeat([]byte{0xff}, suspendBufMax+1),
|
||||||
|
nil, nil, nSpecialPtEquiv},
|
||||||
|
|
||||||
|
{"suspend fill", bytes.Repeat([]byte{0xfe}, suspendBufMax), nil,
|
||||||
|
nil, nil, nSpecialSuspend},
|
||||||
|
{"drop", []byte{0}, nil,
|
||||||
|
nil, syscall.ENOMEM, 0},
|
||||||
|
{"drop error", []byte{0}, nil,
|
||||||
|
stub.UniqueError(1), syscall.ENOMEM, 0},
|
||||||
|
{"resume fill", nil, bytes.Repeat([]byte{0xfe}, suspendBufMax),
|
||||||
|
nil, nil, nSpecialDump - 2},
|
||||||
|
|
||||||
|
{"suspend fill partial", bytes.Repeat([]byte{0xfd}, suspendBufMax-0xf), nil,
|
||||||
|
nil, nil, nSpecialSuspend},
|
||||||
|
{"partial write", bytes.Repeat([]byte{0xad}, 0x1f), nil,
|
||||||
|
nil, syscall.ENOMEM, 0xf},
|
||||||
|
{"full drop", []byte{0}, nil,
|
||||||
|
nil, syscall.ENOMEM, 0},
|
||||||
|
{"resume fill partial", nil, append(bytes.Repeat([]byte{0xfd}, suspendBufMax-0xf), bytes.Repeat([]byte{0xad}, 0xf)...),
|
||||||
|
nil, nil, nSpecialDump - 0x10 - 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
var dw expectWriter
|
||||||
|
|
||||||
|
w := container.Suspendable{Downstream: &dw}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
// these share the same writer, so cannot be subtests
|
||||||
|
t.Logf("writing step %q", tc.name)
|
||||||
|
dw.expect, dw.err = tc.pt, tc.err
|
||||||
|
|
||||||
|
var (
|
||||||
|
gotN int
|
||||||
|
gotErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
wantN := tc.n
|
||||||
|
switch wantN {
|
||||||
|
case nSpecialPtEquiv:
|
||||||
|
wantN = len(tc.pt)
|
||||||
|
gotN, gotErr = w.Write(tc.w)
|
||||||
|
|
||||||
|
case nSpecialWEquiv:
|
||||||
|
wantN = len(tc.w)
|
||||||
|
gotN, gotErr = w.Write(tc.w)
|
||||||
|
|
||||||
|
case nSpecialSuspend:
|
||||||
|
s := w.IsSuspended()
|
||||||
|
if ok := w.Suspend(); s && ok {
|
||||||
|
t.Fatal("Suspend: unexpected success")
|
||||||
|
}
|
||||||
|
|
||||||
|
wantN = len(tc.w)
|
||||||
|
gotN, gotErr = w.Write(tc.w)
|
||||||
|
|
||||||
|
default:
|
||||||
|
if wantN <= nSpecialDump {
|
||||||
|
if !w.IsSuspended() {
|
||||||
|
t.Fatal("IsSuspended unexpected false")
|
||||||
|
}
|
||||||
|
|
||||||
|
resumed, dropped, n, err := w.Resume()
|
||||||
|
if !resumed {
|
||||||
|
t.Fatal("Resume: resumed = false")
|
||||||
|
}
|
||||||
|
if wantDropped := nSpecialDump - wantN; int(dropped) != wantDropped {
|
||||||
|
t.Errorf("Resume: dropped = %d, want %d", dropped, wantDropped)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantN = len(tc.pt)
|
||||||
|
gotN, gotErr = int(n), err
|
||||||
|
} else {
|
||||||
|
gotN, gotErr = w.Write(tc.w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotN != wantN {
|
||||||
|
t.Errorf("Write: n = %d, want %d", gotN, wantN)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(gotErr, tc.wantErr) {
|
||||||
|
t.Errorf("Write: %v", gotErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expectWriter compares Write calls to expect.
|
||||||
|
type expectWriter struct {
|
||||||
|
expect []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *expectWriter) Write(p []byte) (n int, err error) {
|
||||||
|
defer func() { w.expect = nil }()
|
||||||
|
|
||||||
|
n, err = len(p), w.err
|
||||||
|
if w.expect == nil {
|
||||||
|
return 0, errors.New("unexpected call to Write: " + strconv.Quote(string(p)))
|
||||||
|
}
|
||||||
|
if string(p) != string(w.expect) {
|
||||||
|
return 0, errors.New("p = " + strconv.Quote(string(p)) + ", want " + strconv.Quote(string(w.expect)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
package sandbox
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
"syscall"
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotSet = errors.New("environment variable not set")
|
|
||||||
ErrInvalid = errors.New("bad file descriptor")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
||||||
@@ -23,22 +19,26 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrReceiveEnv = errors.New("environment variable not set")
|
||||||
|
)
|
||||||
|
|
||||||
// Receive retrieves setup fd from the environment and receives params.
|
// Receive retrieves setup fd from the environment and receives params.
|
||||||
func Receive(key string, e any, v **os.File) (func() error, error) {
|
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||||
var setup *os.File
|
var setup *os.File
|
||||||
|
|
||||||
if s, ok := os.LookupEnv(key); !ok {
|
if s, ok := os.LookupEnv(key); !ok {
|
||||||
return nil, ErrNotSet
|
return nil, ErrReceiveEnv
|
||||||
} else {
|
} else {
|
||||||
if fd, err := strconv.Atoi(s); err != nil {
|
if fd, err := strconv.Atoi(s); err != nil {
|
||||||
return nil, err
|
return nil, errors.Unwrap(err)
|
||||||
} else {
|
} else {
|
||||||
setup = os.NewFile(uintptr(fd), "setup")
|
setup = os.NewFile(uintptr(fd), "setup")
|
||||||
if setup == nil {
|
if setup == nil {
|
||||||
return nil, ErrInvalid
|
return nil, syscall.EDOM
|
||||||
}
|
}
|
||||||
if v != nil {
|
if fdp != nil {
|
||||||
*v = setup
|
*fdp = setup.Fd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.ErrReceiveEnv) {
|
||||||
|
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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, strconv.ErrSyntax) {
|
||||||
|
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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.EDOM) {
|
||||||
|
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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) })
|
||||||
|
})
|
||||||
|
}
|
||||||
159
container/path.go
Normal file
159
container/path.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"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 err
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if content != nil {
|
||||||
|
_, err = f.Write(content)
|
||||||
|
}
|
||||||
|
return errors.Join(f.Close(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureFile(name string, perm, pperm os.FileMode) error {
|
||||||
|
fi, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return createFile(name, perm, pperm, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
|
||||||
|
err = &os.PathError{Op: "ensure", Path: name, Err: syscall.EISDIR}
|
||||||
|
}
|
||||||
|
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 err
|
||||||
|
} else {
|
||||||
|
d := vfs.NewMountInfoDecoder(r)
|
||||||
|
err0 := f(d)
|
||||||
|
if err = r.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = d.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err0
|
||||||
|
}
|
||||||
|
}
|
||||||
257
container/path_test.go
Normal file
257
container/path_test.go
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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) {
|
||||||
|
t.Run("mkdir", func(t *testing.T) {
|
||||||
|
wantErr := &os.PathError{
|
||||||
|
Op: "mkdir",
|
||||||
|
Path: "/proc/nonexistent",
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("open", func(t *testing.T) {
|
||||||
|
wantErr := &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: "/proc/nonexistent",
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 := &os.PathError{
|
||||||
|
Op: "stat",
|
||||||
|
Path: pathname,
|
||||||
|
Err: syscall.EACCES,
|
||||||
|
}
|
||||||
|
if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(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 := &os.PathError{Op: "ensure", Path: pathname, Err: syscall.EISDIR}
|
||||||
|
if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(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 := &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: nonexistentProc.self + "/mountinfo",
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !reflect.DeepEqual(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 := &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()
|
||||||
|
}
|
||||||
|
}); !reflect.DeepEqual(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 := &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}
|
||||||
|
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !reflect.DeepEqual(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);
|
||||||
194
container/seccomp/libseccomp.go
Normal file
194
container/seccomp/libseccomp.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package seccomp
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo linux pkg-config: --static libseccomp
|
||||||
|
|
||||||
|
#include <libseccomp-helper.h>
|
||||||
|
#include <sys/personality.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PER_LINUX = C.PER_LINUX
|
||||||
|
PER_LINUX32 = C.PER_LINUX32
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidRules = errors.New("invalid native rules slice")
|
||||||
|
)
|
||||||
|
|
||||||
|
// LibraryError represents a libseccomp error.
|
||||||
|
type LibraryError struct {
|
||||||
|
Prefix string
|
||||||
|
Seccomp syscall.Errno
|
||||||
|
Errno error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LibraryError) Error() string {
|
||||||
|
if e.Seccomp == 0 {
|
||||||
|
if e.Errno == nil {
|
||||||
|
panic("invalid libseccomp error")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: %s", e.Prefix, e.Errno)
|
||||||
|
}
|
||||||
|
if e.Errno == nil {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LibraryError) Is(err error) bool {
|
||||||
|
if e == nil {
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
if ef, ok := err.(*LibraryError); ok {
|
||||||
|
return *e == *ef
|
||||||
|
}
|
||||||
|
return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) ||
|
||||||
|
(e.Errno != nil && errors.Is(err, e.Errno))
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
ScmpSyscall = C.int
|
||||||
|
ScmpErrno = C.int
|
||||||
|
)
|
||||||
|
|
||||||
|
// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions.
|
||||||
|
type NativeRule struct {
|
||||||
|
// Syscall is the arch-dependent syscall number to act against.
|
||||||
|
Syscall ScmpSyscall
|
||||||
|
// Errno is the errno value to return when the condition is satisfied.
|
||||||
|
Errno ScmpErrno
|
||||||
|
// Arg is the optional struct scmp_arg_cmp passed to libseccomp.
|
||||||
|
Arg *ScmpArgCmp
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportFlag = C.hakurei_export_flag
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AllowMultiarch allows multiarch/emulation.
|
||||||
|
AllowMultiarch ExportFlag = C.HAKUREI_EXPORT_MULTIARCH
|
||||||
|
// AllowCAN allows AF_CAN.
|
||||||
|
AllowCAN ExportFlag = C.HAKUREI_EXPORT_CAN
|
||||||
|
// AllowBluetooth allows AF_BLUETOOTH.
|
||||||
|
AllowBluetooth ExportFlag = C.HAKUREI_EXPORT_BLUETOOTH
|
||||||
|
)
|
||||||
|
|
||||||
|
var resPrefix = [...]string{
|
||||||
|
0: "",
|
||||||
|
1: "seccomp_init failed",
|
||||||
|
2: "seccomp_arch_add failed",
|
||||||
|
3: "seccomp_arch_add failed (multiarch)",
|
||||||
|
4: "internal libseccomp failure",
|
||||||
|
5: "seccomp_rule_add failed",
|
||||||
|
6: "seccomp_export_bpf failed",
|
||||||
|
7: "seccomp_load failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export streams filter contents to fd, or installs it to the current process if fd < 0.
|
||||||
|
func Export(fd int, rules []NativeRule, flags ExportFlag) error {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return ErrInvalidRules
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
arch C.uint32_t = 0
|
||||||
|
multiarch C.uint32_t = 0
|
||||||
|
)
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "386":
|
||||||
|
arch = C.SCMP_ARCH_X86
|
||||||
|
case "amd64":
|
||||||
|
arch = C.SCMP_ARCH_X86_64
|
||||||
|
multiarch = C.SCMP_ARCH_X86
|
||||||
|
case "arm":
|
||||||
|
arch = C.SCMP_ARCH_ARM
|
||||||
|
case "arm64":
|
||||||
|
arch = C.SCMP_ARCH_AARCH64
|
||||||
|
multiarch = C.SCMP_ARCH_ARM
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret C.int
|
||||||
|
|
||||||
|
rulesPinner := new(runtime.Pinner)
|
||||||
|
for i := range rules {
|
||||||
|
rule := &rules[i]
|
||||||
|
rulesPinner.Pin(rule)
|
||||||
|
if rule.Arg != nil {
|
||||||
|
rulesPinner.Pin(rule.Arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res, err := C.hakurei_export_filter(
|
||||||
|
&ret, C.int(fd),
|
||||||
|
arch, multiarch,
|
||||||
|
(*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])),
|
||||||
|
C.size_t(len(rules)),
|
||||||
|
flags,
|
||||||
|
)
|
||||||
|
rulesPinner.Unpin()
|
||||||
|
|
||||||
|
if prefix := resPrefix[res]; prefix != "" {
|
||||||
|
return &LibraryError{
|
||||||
|
prefix,
|
||||||
|
-syscall.Errno(ret),
|
||||||
|
err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScmpCompare is the equivalent of scmp_compare;
|
||||||
|
// Comparison operators
|
||||||
|
type ScmpCompare = C.enum_scmp_compare
|
||||||
|
|
||||||
|
const (
|
||||||
|
_SCMP_CMP_MIN = C._SCMP_CMP_MIN
|
||||||
|
|
||||||
|
// not equal
|
||||||
|
SCMP_CMP_NE = C.SCMP_CMP_NE
|
||||||
|
// less than
|
||||||
|
SCMP_CMP_LT = C.SCMP_CMP_LT
|
||||||
|
// less than or equal
|
||||||
|
SCMP_CMP_LE = C.SCMP_CMP_LE
|
||||||
|
// equal
|
||||||
|
SCMP_CMP_EQ = C.SCMP_CMP_EQ
|
||||||
|
// greater than or equal
|
||||||
|
SCMP_CMP_GE = C.SCMP_CMP_GE
|
||||||
|
// greater than
|
||||||
|
SCMP_CMP_GT = C.SCMP_CMP_GT
|
||||||
|
// masked equality
|
||||||
|
SCMP_CMP_MASKED_EQ = C.SCMP_CMP_MASKED_EQ
|
||||||
|
|
||||||
|
_SCMP_CMP_MAX = C._SCMP_CMP_MAX
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScmpDatum is the equivalent of scmp_datum_t;
|
||||||
|
// Argument datum
|
||||||
|
type ScmpDatum uint64
|
||||||
|
|
||||||
|
// ScmpArgCmp is the equivalent of struct scmp_arg_cmp;
|
||||||
|
// Argument / Value comparison definition
|
||||||
|
type ScmpArgCmp struct {
|
||||||
|
// argument number, starting at 0
|
||||||
|
Arg C.uint
|
||||||
|
// the comparison op, e.g. SCMP_CMP_*
|
||||||
|
Op ScmpCompare
|
||||||
|
|
||||||
|
DatumA, DatumB ScmpDatum
|
||||||
|
}
|
||||||
|
|
||||||
|
// only used for testing
|
||||||
|
func syscallResolveName(s string) (trap int) {
|
||||||
|
v := C.CString(s)
|
||||||
|
trap = int(C.seccomp_syscall_resolve_name(v))
|
||||||
|
C.free(unsafe.Pointer(v))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
94
container/seccomp/libseccomp_test.go
Normal file
94
container/seccomp/libseccomp_test.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package seccomp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha512"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"slices"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "hakurei.app/container/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExport(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
flags ExportFlag
|
||||||
|
presets FilterPreset
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"everything", AllowMultiarch | AllowCAN |
|
||||||
|
AllowBluetooth, PresetExt |
|
||||||
|
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
|
||||||
|
PresetLinux32, false},
|
||||||
|
|
||||||
|
{"compat", 0, 0, false},
|
||||||
|
{"base", 0, PresetExt, false},
|
||||||
|
{"strict", 0, PresetStrict, false},
|
||||||
|
{"strict compat", 0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel, false},
|
||||||
|
{"hakurei default", 0, PresetExt | PresetDenyDevel, false},
|
||||||
|
{"hakurei tty", 0, PresetExt | PresetDenyNS | PresetDenyDevel, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := New(Preset(tc.presets, tc.flags), tc.flags)
|
||||||
|
want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
|
||||||
|
digest := sha512.New()
|
||||||
|
|
||||||
|
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
|
||||||
|
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := e.Close(); err != nil {
|
||||||
|
t.Errorf("Close: error = %v", err)
|
||||||
|
}
|
||||||
|
if got := digest.Sum(nil); !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("Export() hash = %x, want %x",
|
||||||
|
got, want)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("close without use", func(t *testing.T) {
|
||||||
|
e := New(Preset(0, 0), 0)
|
||||||
|
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
|
||||||
|
t.Errorf("Close: error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("close partial read", func(t *testing.T) {
|
||||||
|
e := New(Preset(0, 0), 0)
|
||||||
|
if _, err := e.Read(nil); err != nil {
|
||||||
|
t.Errorf("Read: error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// the underlying implementation uses buffered io, so the outcome of this is nondeterministic;
|
||||||
|
// that is not harmful however, so both outcomes are checked for here
|
||||||
|
if err := e.Close(); err != nil &&
|
||||||
|
(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
|
||||||
|
t.Errorf("Close: error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkExport(b *testing.B) {
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
for b.Loop() {
|
||||||
|
e := New(
|
||||||
|
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
|
||||||
|
AllowMultiarch|AllowCAN|AllowBluetooth),
|
||||||
|
AllowMultiarch|AllowCAN|AllowBluetooth)
|
||||||
|
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil {
|
||||||
|
b.Fatalf("cannot export: %v", err)
|
||||||
|
}
|
||||||
|
if err := e.Close(); err != nil {
|
||||||
|
b.Fatalf("cannot close exporter: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
container/seccomp/mksysnum_linux.pl
Executable file
89
container/seccomp/mksysnum_linux.pl
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
# Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style
|
||||||
|
# license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use POSIX ();
|
||||||
|
|
||||||
|
my $command = "mksysnum_linux.pl ". join(' ', @ARGV);
|
||||||
|
my $uname_arch = (POSIX::uname)[4];
|
||||||
|
my %syscall_cutoff_arch = (
|
||||||
|
"x86_64" => 302,
|
||||||
|
"aarch64" => 281,
|
||||||
|
);
|
||||||
|
|
||||||
|
print <<EOF;
|
||||||
|
// $command
|
||||||
|
// Code generated by the command above; DO NOT EDIT.
|
||||||
|
|
||||||
|
package seccomp
|
||||||
|
|
||||||
|
import . "syscall"
|
||||||
|
|
||||||
|
var syscallNum = map[string]int{
|
||||||
|
EOF
|
||||||
|
|
||||||
|
my $offset = 0;
|
||||||
|
my $state = -1;
|
||||||
|
|
||||||
|
sub fmt {
|
||||||
|
my ($name, $num) = @_;
|
||||||
|
if($num > 999){
|
||||||
|
# ignore deprecated syscalls that are no longer implemented
|
||||||
|
# https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/unistd.h?id=refs/heads/master#n716
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(my $name_upper = $name) =~ y/a-z/A-Z/;
|
||||||
|
$num = $num + $offset;
|
||||||
|
if($num > $syscall_cutoff_arch{$uname_arch}){ # not wired in Go standard library
|
||||||
|
if($state < 0){
|
||||||
|
print " \"$name\": SYS_$name_upper,\n";
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
print " SYS_$name_upper = $num;\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elsif($state < 0){
|
||||||
|
print " \"$name\": SYS_$name_upper,\n";
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GENERATE:
|
||||||
|
|
||||||
|
my $prev;
|
||||||
|
open(GCC, "gcc -E -dD $ARGV[0] |") || die "can't run gcc";
|
||||||
|
while(<GCC>){
|
||||||
|
if(/^#define __NR_Linux\s+([0-9]+)/){
|
||||||
|
# mips/mips64: extract offset
|
||||||
|
$offset = $1;
|
||||||
|
}
|
||||||
|
elsif(/^#define __NR_syscalls\s+/) {
|
||||||
|
# ignore redefinitions of __NR_syscalls
|
||||||
|
}
|
||||||
|
elsif(/^#define __NR_(\w+)\s+([0-9]+)/){
|
||||||
|
$prev = $2;
|
||||||
|
fmt($1, $2);
|
||||||
|
}
|
||||||
|
elsif(/^#define __NR3264_(\w+)\s+([0-9]+)/){
|
||||||
|
$prev = $2;
|
||||||
|
fmt($1, $2);
|
||||||
|
}
|
||||||
|
elsif(/^#define __NR_(\w+)\s+\(\w+\+\s*([0-9]+)\)/){
|
||||||
|
fmt($1, $prev+$2)
|
||||||
|
}
|
||||||
|
elsif(/^#define __NR_(\w+)\s+\(__NR_Linux \+ ([0-9]+)/){
|
||||||
|
fmt($1, $2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($state < 0){
|
||||||
|
$state = $state + 1;
|
||||||
|
print "}\n\nconst (\n";
|
||||||
|
goto GENERATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
print ")";
|
||||||
229
container/seccomp/presets.go
Normal file
229
container/seccomp/presets.go
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
package seccomp
|
||||||
|
|
||||||
|
/* flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 */
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilterPreset int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PresetExt are project-specific extensions.
|
||||||
|
PresetExt FilterPreset = 1 << iota
|
||||||
|
// PresetDenyNS denies namespace setup syscalls.
|
||||||
|
PresetDenyNS
|
||||||
|
// PresetDenyTTY denies faking input.
|
||||||
|
PresetDenyTTY
|
||||||
|
// PresetDenyDevel denies development-related syscalls.
|
||||||
|
PresetDenyDevel
|
||||||
|
// PresetLinux32 sets PER_LINUX32.
|
||||||
|
PresetLinux32
|
||||||
|
)
|
||||||
|
|
||||||
|
func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
|
||||||
|
allowedPersonality := PER_LINUX
|
||||||
|
if presets&PresetLinux32 != 0 {
|
||||||
|
allowedPersonality = PER_LINUX32
|
||||||
|
}
|
||||||
|
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))
|
||||||
|
|
||||||
|
l := len(presetCommon)
|
||||||
|
if presets&PresetDenyNS != 0 {
|
||||||
|
l += len(presetNamespace)
|
||||||
|
}
|
||||||
|
if presets&PresetDenyTTY != 0 {
|
||||||
|
l += len(presetTTY)
|
||||||
|
}
|
||||||
|
if presets&PresetDenyDevel != 0 {
|
||||||
|
l += len(presetDevelFinal)
|
||||||
|
}
|
||||||
|
if flags&AllowMultiarch == 0 {
|
||||||
|
l += len(presetEmu)
|
||||||
|
}
|
||||||
|
if presets&PresetExt != 0 {
|
||||||
|
l += len(presetCommonExt)
|
||||||
|
if presets&PresetDenyNS != 0 {
|
||||||
|
l += len(presetNamespaceExt)
|
||||||
|
}
|
||||||
|
if flags&AllowMultiarch == 0 {
|
||||||
|
l += len(presetEmuExt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = make([]NativeRule, 0, l)
|
||||||
|
rules = append(rules, presetCommon...)
|
||||||
|
if presets&PresetDenyNS != 0 {
|
||||||
|
rules = append(rules, presetNamespace...)
|
||||||
|
}
|
||||||
|
if presets&PresetDenyTTY != 0 {
|
||||||
|
rules = append(rules, presetTTY...)
|
||||||
|
}
|
||||||
|
if presets&PresetDenyDevel != 0 {
|
||||||
|
rules = append(rules, presetDevelFinal...)
|
||||||
|
}
|
||||||
|
if flags&AllowMultiarch == 0 {
|
||||||
|
rules = append(rules, presetEmu...)
|
||||||
|
}
|
||||||
|
if presets&PresetExt != 0 {
|
||||||
|
rules = append(rules, presetCommonExt...)
|
||||||
|
if presets&PresetDenyNS != 0 {
|
||||||
|
rules = append(rules, presetNamespaceExt...)
|
||||||
|
}
|
||||||
|
if flags&AllowMultiarch == 0 {
|
||||||
|
rules = append(rules, presetEmuExt...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
presetCommon = []NativeRule{
|
||||||
|
/* Block dmesg */
|
||||||
|
{ScmpSyscall(SYS_SYSLOG), ScmpErrno(EPERM), nil},
|
||||||
|
/* Useless old syscall */
|
||||||
|
{ScmpSyscall(SYS_USELIB), ScmpErrno(EPERM), nil},
|
||||||
|
/* Don't allow disabling accounting */
|
||||||
|
{ScmpSyscall(SYS_ACCT), ScmpErrno(EPERM), nil},
|
||||||
|
/* Don't allow reading current quota use */
|
||||||
|
{ScmpSyscall(SYS_QUOTACTL), ScmpErrno(EPERM), nil},
|
||||||
|
|
||||||
|
/* Don't allow access to the kernel keyring */
|
||||||
|
{ScmpSyscall(SYS_ADD_KEY), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_KEYCTL), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_REQUEST_KEY), ScmpErrno(EPERM), nil},
|
||||||
|
|
||||||
|
/* Scary VM/NUMA ops */
|
||||||
|
{ScmpSyscall(SYS_MOVE_PAGES), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_MBIND), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_GET_MEMPOLICY), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SET_MEMPOLICY), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_MIGRATE_PAGES), ScmpErrno(EPERM), nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hakurei: project-specific extensions */
|
||||||
|
presetCommonExt = []NativeRule{
|
||||||
|
/* system calls for changing the system clock */
|
||||||
|
{ScmpSyscall(SYS_ADJTIMEX), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_CLOCK_ADJTIME), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_CLOCK_ADJTIME64), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_CLOCK_SETTIME), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_CLOCK_SETTIME64), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETTIMEOFDAY), ScmpErrno(EPERM), nil},
|
||||||
|
|
||||||
|
/* loading and unloading of kernel modules */
|
||||||
|
{ScmpSyscall(SYS_DELETE_MODULE), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_FINIT_MODULE), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_INIT_MODULE), ScmpErrno(EPERM), nil},
|
||||||
|
|
||||||
|
/* system calls for rebooting and reboot preparation */
|
||||||
|
{ScmpSyscall(SYS_KEXEC_FILE_LOAD), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_KEXEC_LOAD), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_REBOOT), ScmpErrno(EPERM), nil},
|
||||||
|
|
||||||
|
/* system calls for enabling/disabling swap devices */
|
||||||
|
{ScmpSyscall(SYS_SWAPOFF), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SWAPON), ScmpErrno(EPERM), nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
presetNamespace = []NativeRule{
|
||||||
|
/* Don't allow subnamespace setups: */
|
||||||
|
{ScmpSyscall(SYS_UNSHARE), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETNS), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_MOUNT), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_UMOUNT), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_UMOUNT2), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_PIVOT_ROOT), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_CHROOT), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_CLONE), ScmpErrno(EPERM),
|
||||||
|
&ScmpArgCmp{cloneArg, SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER}},
|
||||||
|
|
||||||
|
/* seccomp can't look into clone3()'s struct clone_args to check whether
|
||||||
|
* the flags are OK, so we have no choice but to block clone3().
|
||||||
|
* Return ENOSYS so user-space will fall back to clone().
|
||||||
|
* (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d)
|
||||||
|
*/
|
||||||
|
{ScmpSyscall(SYS_CLONE3), ScmpErrno(ENOSYS), nil},
|
||||||
|
|
||||||
|
/* New mount manipulation APIs can also change our VFS. There's no
|
||||||
|
* legitimate reason to do these in the sandbox, so block all of them
|
||||||
|
* rather than thinking about which ones might be dangerous.
|
||||||
|
* (CVE-2021-41133) */
|
||||||
|
{ScmpSyscall(SYS_OPEN_TREE), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_MOVE_MOUNT), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_FSOPEN), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_FSCONFIG), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_FSMOUNT), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_FSPICK), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_MOUNT_SETATTR), ScmpErrno(ENOSYS), nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hakurei: project-specific extensions */
|
||||||
|
presetNamespaceExt = []NativeRule{
|
||||||
|
/* changing file ownership */
|
||||||
|
{ScmpSyscall(SYS_CHOWN), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_CHOWN32), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_FCHOWN), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_FCHOWN32), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_FCHOWNAT), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_LCHOWN), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_LCHOWN32), ScmpErrno(EPERM), nil},
|
||||||
|
|
||||||
|
/* system calls for changing user ID and group ID credentials */
|
||||||
|
{ScmpSyscall(SYS_SETGID), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETGID32), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETGROUPS), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETGROUPS32), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETREGID), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETREGID32), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETRESGID), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETRESGID32), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETRESUID), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETRESUID32), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETREUID), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETREUID32), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETUID), ScmpErrno(EPERM), nil},
|
||||||
|
{ScmpSyscall(SYS_SETUID32), ScmpErrno(EPERM), nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
presetTTY = []NativeRule{
|
||||||
|
/* Don't allow faking input to the controlling tty (CVE-2017-5226) */
|
||||||
|
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
|
||||||
|
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCSTI}},
|
||||||
|
/* In the unlikely event that the controlling tty is a Linux virtual
|
||||||
|
* console (/dev/tty2 or similar), copy/paste operations have an effect
|
||||||
|
* similar to TIOCSTI (CVE-2023-28100) */
|
||||||
|
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
|
||||||
|
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCLINUX}},
|
||||||
|
}
|
||||||
|
|
||||||
|
presetEmu = []NativeRule{
|
||||||
|
/* modify_ldt is a historic source of interesting information leaks,
|
||||||
|
* so it's disabled as a hardening measure.
|
||||||
|
* However, it is required to run old 16-bit applications
|
||||||
|
* as well as some Wine patches, so it's allowed in multiarch. */
|
||||||
|
{ScmpSyscall(SYS_MODIFY_LDT), ScmpErrno(EPERM), nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hakurei: project-specific extensions */
|
||||||
|
presetEmuExt = []NativeRule{
|
||||||
|
{ScmpSyscall(SYS_SUBPAGE_PROT), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_SWITCH_ENDIAN), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_VM86), ScmpErrno(ENOSYS), nil},
|
||||||
|
{ScmpSyscall(SYS_VM86OLD), ScmpErrno(ENOSYS), nil},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func presetDevel(allowedPersonality ScmpDatum) []NativeRule {
|
||||||
|
return []NativeRule{
|
||||||
|
/* Profiling operations; we expect these to be done by tools from outside
|
||||||
|
* the sandbox. In particular perf has been the source of many CVEs. */
|
||||||
|
{ScmpSyscall(SYS_PERF_EVENT_OPEN), ScmpErrno(EPERM), nil},
|
||||||
|
/* Don't allow you to switch to bsd emulation or whatnot */
|
||||||
|
{ScmpSyscall(SYS_PERSONALITY), ScmpErrno(EPERM),
|
||||||
|
&ScmpArgCmp{0, SCMP_CMP_NE, allowedPersonality, 0}},
|
||||||
|
|
||||||
|
{ScmpSyscall(SYS_PTRACE), ScmpErrno(EPERM), nil},
|
||||||
|
}
|
||||||
|
}
|
||||||
7
container/seccomp/presets_clone_backwards2.go
Normal file
7
container/seccomp/presets_clone_backwards2.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build s390 || s390x
|
||||||
|
|
||||||
|
package seccomp
|
||||||
|
|
||||||
|
/* Architectures with CONFIG_CLONE_BACKWARDS2: the child stack
|
||||||
|
* and flags arguments are reversed so the flags come second */
|
||||||
|
const cloneArg = 1
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user