Compare commits
299 Commits
83e72c2b59
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
e6967b8bbb
|
|||
|
d2f9a9b83b
|
|||
|
1b5ecd9eaf
|
|||
|
82561d62b6
|
|||
|
eec021cc4b
|
|||
|
a1d98823f8
|
|||
|
255b77d91d
|
|||
|
f84ec5a3f8
|
|||
|
eb22a8bcc1
|
|||
|
31aef905fa
|
|||
|
a6887f7253
|
|||
|
69bd581af7
|
|||
|
26b7afc890
|
|||
|
d5532aade0
|
|||
|
0c5409aec7
|
|||
|
1a8840bebc
|
|||
|
1fb453dffe
|
|||
|
e03d702d08
|
|||
|
241dc964a6
|
|||
|
8ef71e14d5
|
|||
|
972f4006f0
|
|||
|
9a8a047908
|
|||
|
863bf69ad3
|
|||
|
0e957cc9c1
|
|||
|
aa454b158f
|
|||
|
7007bd6a1c
|
|||
|
00efc95ee7
|
|||
|
b380bb248c
|
|||
|
87e008d56d
|
|||
|
3992073212
|
|||
|
ef80b19f2f
|
|||
|
717771ae80
|
|||
|
bf5772bd8a
|
|||
|
9a7c81a44e
|
|||
|
b7e991de5b
|
|||
|
6c1205106d
|
|||
|
2ffca6984a
|
|||
|
dde2516304
|
|||
|
f30a439bcd
|
|||
|
008e9e7fc5
|
|||
|
23aefcd759
|
|||
|
cb8b886446
|
|||
|
5979d8b1e0
|
|||
|
e587112e63
|
|||
|
d6cf736abf
|
|||
|
15011c4173
|
|||
|
31b7ddd122
|
|||
|
c460892cbd
|
|||
|
6309469e93
|
|||
|
0d7c1a9a43
|
|||
|
ae6f5ede19
|
|||
|
807d511c8b
|
|||
|
2f4f21fb18
|
|||
|
9967909460
|
|||
|
c806f43881
|
|||
|
584405f7cc
|
|||
|
50127ed5f9
|
|||
|
b5eff27c40
|
|||
|
74ba183256
|
|||
|
f885dede9b
|
|||
|
e9a7cd526f
|
|||
|
12be7bc78e
|
|||
|
0ba8be659f
|
|||
|
022242a84a
|
|||
|
8aeb06f53c
|
|||
|
4036da3b5c
|
|||
|
986105958c
|
|||
|
ecdd4d8202
|
|||
|
bdee0c3921
|
|||
|
48f634d046
|
|||
|
2a46f5bb12
|
|||
|
7f2c0af5ad
|
|||
|
297b444dfb
|
|||
|
89a05909a4
|
|||
|
f772940768
|
|||
|
8886c40974
|
|||
|
8b62e08b44
|
|||
|
72c59f9229
|
|||
|
ff3cfbb437
|
|||
|
c13eb70d7d
|
|||
|
389402f955
|
|||
|
660a2898dc
|
|||
|
faf59e12c0
|
|||
|
d97a03c7c6
|
|||
|
a102178019
|
|||
|
e400862a12
|
|||
|
184e9db2b2
|
|||
|
605d018be2
|
|||
|
78aaae7ee0
|
|||
|
5c82f1ed3e
|
|||
|
f8502c3ece
|
|||
|
996b42634d
|
|||
|
300571af47
|
|||
|
32c90ef4e7
|
|||
|
2a4e2724a3
|
|||
|
d613257841
|
|||
|
18644d90be
|
|||
|
52fcc48ac1
|
|||
|
8b69bcd215
|
|||
|
2dd49c437c
|
|||
|
92852d8235
|
|||
|
371dd5b938
|
|||
|
4836d570ae
|
|||
|
985f9442e6
|
|||
|
67eb28466d
|
|||
|
c326c3f97d
|
|||
|
971c79bb80
|
|||
|
f86d868274
|
|||
|
33940265a6
|
|||
|
b39f3aeb59
|
|||
|
61dbfeffe7
|
|||
|
532feb4bfa
|
|||
|
ec5e91b8c9
|
|||
|
ee51320abf
|
|||
|
5c4058d5ac
|
|||
|
e732dca762
|
|||
|
a9adcd914b
|
|||
|
3dd4ff29c8
|
|||
|
61d86c5e10
|
|||
|
d097eaa28f
|
|||
|
ad3576c164
|
|||
|
b989a4601a
|
|||
|
a11237b158
|
|||
|
40f00d570e
|
|||
|
0eb1bc6301
|
|||
|
1eb837eab8
|
|||
|
0a4e633db2
|
|||
|
e8809125d4
|
|||
|
806ce18c0a
|
|||
|
b71d2bf534
|
|||
|
46059b1840
|
|||
|
d2c329bcea
|
|||
|
2d379b5a38
|
|||
|
75e0c5d406
|
|||
|
770b37ae16
|
|||
|
c638193268
|
|||
|
8c3a817881
|
|||
|
e2fce321c1
|
|||
|
241702ae3a
|
|||
|
d21d9c5b1d
|
|||
|
a70daf2250
|
|||
|
632b18addd
|
|||
|
a57a7a6a16
|
|||
|
5098b12e4a
|
|||
|
9ddf5794dd
|
|||
|
b74a08dda9
|
|||
|
1b9408864f
|
|||
|
cc89dbdf63
|
|||
|
228f3301f2
|
|||
|
07181138e5
|
|||
|
816b372f14
|
|||
|
d7eddd54a2
|
|||
|
7c063833e0
|
|||
|
af3619d440
|
|||
|
528674cb6e
|
|||
|
70c9757e26
|
|||
|
c83a7e2efc
|
|||
|
904208b87f
|
|||
|
007b52d81f
|
|||
|
3385538142
|
|||
|
24618ab9a1
|
|||
|
9ce4706a07
|
|||
|
9a1f8e129f
|
|||
|
ee10860357
|
|||
|
44277dc0f1
|
|||
|
bc54db54d2
|
|||
|
bf07b7cd9e
|
|||
|
5d3c8dcc92
|
|||
|
48feca800f
|
|||
|
42de09e896
|
|||
|
1576fea8a3
|
|||
|
ae522ab364
|
|||
|
273d97af85
|
|||
|
891316d924
|
|||
|
9f5dad1998
|
|||
|
6e7ddb2d2e
|
|||
|
bac4e67867
|
|||
|
4230281194
|
|||
|
e64e7608ca
|
|||
|
10a21ce3ef
|
|||
|
0f1f0e4364
|
|||
|
f9bf20a3c7
|
|||
|
73c1a83032
|
|||
|
f443d315ad
|
|||
|
9e18d1de77
|
|||
|
2647a71be1
|
|||
|
7c60a4d8e8
|
|||
|
4bb5d9780f
|
|||
|
f41fd94628
|
|||
|
94895bbacb
|
|||
|
f332200ca4
|
|||
|
2eff470091
|
|||
|
a092b042ab
|
|||
|
e94b09d337
|
|||
|
5d9e669d97
|
|||
|
f1002157a5
|
|||
|
4133b555ba
|
|||
|
9b1a60b5c9
|
|||
|
beb3918809
|
|||
|
2871426df2
|
|||
|
e048f31baa
|
|||
|
6af8b8859f
|
|||
|
f38ba7e923
|
|||
|
d22145a392
|
|||
|
29c3f8becb
|
|||
|
be16970e77
|
|||
|
df266527f1
|
|||
|
c8ed7aae6e
|
|||
|
61e58aa14d
|
|||
|
9e15898c8f
|
|||
|
f7bd6a5a41
|
|||
|
ea853e21d9
|
|||
|
0bd9b9e8fe
|
|||
|
39e32799b3
|
|||
|
9953768de5
|
|||
|
0d3652b793
|
|||
|
d8e9d71f87
|
|||
|
558974b996
|
|||
|
4de4049713
|
|||
|
2d4cabe786
|
|||
|
80f9b62d25
|
|||
|
673b648bd3
|
|||
|
45ad788c6d
|
|||
|
56539d8db5
|
|||
|
840ceb615a
|
|||
|
741d011543
|
|||
|
d050b3de25
|
|||
|
5de28800ad
|
|||
|
8e50293ab7
|
|||
|
12c6d66bfd
|
|||
|
d7d2bd33ed
|
|||
|
c21a4cff14
|
|||
|
4fa38d6063
|
|||
|
6d4ac3d9fd
|
|||
|
a5d2f040fb
|
|||
|
c62689e17f
|
|||
|
39dc8e7bd8
|
|||
|
5a732d153e
|
|||
|
b4549c72be
|
|||
|
1818dc3a4c
|
|||
|
65094b63cd
|
|||
|
f0a082ec84
|
|||
|
751aa350ee
|
|||
|
e6cd2bb2a8
|
|||
|
0fb72e5d99
|
|||
|
71135f339a
|
|||
|
b6af8caffe
|
|||
|
e1a3549ea0
|
|||
|
8bf162820b
|
|||
|
dccb366608
|
|||
|
83c8f0488b
|
|||
|
478b27922c
|
|||
|
ba1498cd18
|
|||
|
eda4d612c2
|
|||
|
2e7e160683
|
|||
|
79957f8ea7
|
|||
|
7e52463445
|
|||
|
89970f5197
|
|||
|
35037705a9
|
|||
|
647c6ea21b
|
|||
|
416d93e880
|
|||
|
312753924b
|
|||
|
54308f79d2
|
|||
|
dfa3217037
|
|||
|
8000a2febb
|
|||
|
7bd48d3489
|
|||
|
b5eaeac11a
|
|||
|
a9986aab6a
|
|||
|
ff30a5ab5d
|
|||
|
eb0c16dd8c
|
|||
|
4fa1e97026
|
|||
|
64b6dc41ba
|
|||
|
c64b8163e7
|
|||
|
9d9a165379
|
|||
|
d0dff1cac9
|
|||
|
3c80fd2b0f
|
|||
|
ef81828e0c
|
|||
|
2978a6f046
|
|||
|
dfd9467523
|
|||
|
53571f030e
|
|||
|
aa164081e1
|
|||
|
9a10eeab90
|
|||
|
d1f83f40d6
|
|||
|
a748d40745
|
|||
|
648e1d641a
|
|||
|
3c327084d3
|
|||
|
ffaa12b9d8
|
|||
|
bf95127332
|
|||
|
e0f321b2c4
|
|||
|
2c9c7fee5b
|
|||
|
d0400f3c81
|
|||
|
e9b0f9faef
|
|||
|
e85be67fd9
|
|||
|
7e69893264
|
|||
|
38a3e6af03
|
|||
|
90cb01b274
|
|||
|
b1e1d5627e
|
|||
|
3ae2ab652e
|
|||
|
db71fbe22b
|
@@ -20,5 +20,5 @@ jobs:
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: |-
|
||||
result/fortify-**
|
||||
result/hakurei-**
|
||||
api_key: '${{secrets.RELEASE_TOKEN}}'
|
||||
|
||||
@@ -5,25 +5,107 @@ on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run NixOS test
|
||||
hakurei:
|
||||
name: Hakurei
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
nix --print-build-logs --experimental-features 'nix-command flakes' flake check
|
||||
nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.nixos-tests
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hakurei
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "nixos-vm-output"
|
||||
name: "hakurei-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
race:
|
||||
name: Hakurei (race detector)
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "hakurei-race-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
sandbox:
|
||||
name: Sandbox
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "sandbox-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
sandbox-race:
|
||||
name: Sandbox (race detector)
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox-race
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "sandbox-race-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
planterette:
|
||||
name: Planterette
|
||||
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.planterette
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "planterette-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
check:
|
||||
name: Flake checks
|
||||
needs:
|
||||
- hakurei
|
||||
- race
|
||||
- sandbox
|
||||
- sandbox-race
|
||||
- planterette
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run checks
|
||||
run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check
|
||||
|
||||
dist:
|
||||
name: Create distribution
|
||||
runs-on: nix
|
||||
@@ -34,15 +116,15 @@ jobs:
|
||||
- name: Build for test
|
||||
id: build-test
|
||||
run: >-
|
||||
export FORTIFY_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$FORTIFY_REV'"; # version = /' package.nix &&
|
||||
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
|
||||
nix build --print-out-paths --print-build-logs .#dist &&
|
||||
mv package.nix.old package.nix &&
|
||||
echo "rev=$FORTIFY_REV" >> $GITHUB_OUTPUT
|
||||
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload test build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "fortify-${{ steps.build-test.outputs.rev }}"
|
||||
name: "hakurei-${{ steps.build-test.outputs.rev }}"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
1
.github/workflows/README
vendored
Normal file
1
.github/workflows/README
vendored
Normal file
@@ -0,0 +1 @@
|
||||
This port is solely for releasing to the github mirror and serves no purpose during development.
|
||||
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
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,7 +5,7 @@
|
||||
*.so
|
||||
*.dylib
|
||||
*.pkg
|
||||
/fortify
|
||||
/hakurei
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
@@ -26,7 +26,7 @@ go.work.sum
|
||||
.vscode
|
||||
|
||||
# go generate
|
||||
security-context-v1-protocol.*
|
||||
/cmd/hakurei/LICENSE
|
||||
|
||||
# release
|
||||
/dist/fortify-*
|
||||
/dist/hakurei-*
|
||||
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:
|
||||
|
||||
|
||||
105
README.md
105
README.md
@@ -1,77 +1,79 @@
|
||||
Fortify
|
||||
=======
|
||||
<p align="center">
|
||||
<a href="https://git.gensokyo.uk/security/hakurei">
|
||||
<picture>
|
||||
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](https://pkg.go.dev/git.gensokyo.uk/security/fortify)
|
||||
[](https://goreportcard.com/report/git.gensokyo.uk/security/fortify)
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
|
||||
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
|
||||
</p>
|
||||
|
||||
Lets you run graphical applications as another user in a confined environment with a nice NixOS
|
||||
module to configure target users and provide launchers and desktop files for your privileged user.
|
||||
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
|
||||
It also implements [planterette (WIP)](cmd/planterette), a self-contained Android-like package manager with modern security features.
|
||||
|
||||
Why would you want this?
|
||||
## NixOS Module usage
|
||||
|
||||
- It protects the desktop environment from applications.
|
||||
|
||||
- It protects applications from each other.
|
||||
|
||||
- It provides UID isolation on top of the standard application sandbox.
|
||||
|
||||
If you have a flakes-enabled nix environment, you can try out the tool by running:
|
||||
|
||||
```shell
|
||||
nix run git+https://git.gensokyo.uk/security/fortify -- help
|
||||
```
|
||||
|
||||
## Module usage
|
||||
|
||||
The NixOS module currently requires home-manager to function correctly.
|
||||
|
||||
Full module documentation can be found [here](options.md).
|
||||
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
|
||||
|
||||
To use the module, import it into your configuration with
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
|
||||
fortify = {
|
||||
url = "git+https://git.gensokyo.uk/security/fortify";
|
||||
hakurei = {
|
||||
url = "git+https://git.gensokyo.uk/security/hakurei";
|
||||
|
||||
# Optional but recommended to limit the size of your system closure.
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, fortify, ... }:
|
||||
outputs = { self, nixpkgs, hakurei, ... }:
|
||||
{
|
||||
nixosConfigurations.fortify = nixpkgs.lib.nixosSystem {
|
||||
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
fortify.nixosModules.fortify
|
||||
hakurei.nixosModules.hakurei
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This adds the `environment.fortify` option:
|
||||
This adds the `environment.hakurei` option:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
environment.fortify = {
|
||||
environment.hakurei = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/persist/module/fortify";
|
||||
stateDir = "/var/lib/hakurei";
|
||||
users = {
|
||||
alice = 0;
|
||||
nixos = 10;
|
||||
};
|
||||
|
||||
apps = [
|
||||
commonPaths = [
|
||||
{
|
||||
src = "/sdcard";
|
||||
write = true;
|
||||
}
|
||||
];
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
|
||||
apps = {
|
||||
"org.chromium.Chromium" = {
|
||||
name = "chromium";
|
||||
id = "org.chromium.Chromium";
|
||||
identity = 1;
|
||||
packages = [ pkgs.chromium ];
|
||||
userns = true;
|
||||
mapRealUid = true;
|
||||
@@ -104,16 +106,20 @@ This adds the `environment.fortify` option:
|
||||
broadcast = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"org.claws_mail.Claws-Mail" = {
|
||||
name = "claws-mail";
|
||||
id = "org.claws_mail.Claws-Mail";
|
||||
identity = 2;
|
||||
packages = [ pkgs.claws-mail ];
|
||||
gpu = false;
|
||||
capability.pulse = false;
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"org.weechat" = {
|
||||
name = "weechat";
|
||||
identity = 3;
|
||||
shareUid = true;
|
||||
packages = [ pkgs.weechat ];
|
||||
capability = {
|
||||
wayland = false;
|
||||
@@ -121,10 +127,12 @@ This adds the `environment.fortify` option:
|
||||
dbus = true;
|
||||
pulse = false;
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"dev.vencord.Vesktop" = {
|
||||
name = "discord";
|
||||
id = "dev.vencord.Vesktop";
|
||||
identity = 3;
|
||||
shareUid = true;
|
||||
packages = [ pkgs.vesktop ];
|
||||
share = pkgs.vesktop;
|
||||
command = "vesktop --ozone-platform-hint=wayland";
|
||||
@@ -142,9 +150,12 @@ This adds the `environment.fortify` option:
|
||||
};
|
||||
system.filter = true;
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"io.looking-glass" = {
|
||||
name = "looking-glass-client";
|
||||
identity = 4;
|
||||
useCommonPaths = false;
|
||||
groups = [ "plugdev" ];
|
||||
extraPaths = [
|
||||
{
|
||||
@@ -155,8 +166,8 @@ This adds the `environment.fortify` option:
|
||||
extraConfig = {
|
||||
programs.looking-glass-client.enable = true;
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
196
acl/c.go
196
acl/c.go
@@ -1,196 +0,0 @@
|
||||
package acl
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: --static libacl
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <sys/acl.h>
|
||||
#include <acl/libacl.h>
|
||||
|
||||
static acl_t _go_acl_get_file(const char *path_p, acl_type_t type) {
|
||||
acl_t acl = acl_get_file(path_p, type);
|
||||
free((void *)path_p);
|
||||
return acl;
|
||||
}
|
||||
|
||||
static int _go_acl_set_file(const char *path_p, acl_type_t type, acl_t acl) {
|
||||
if (acl_valid(acl) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = acl_set_file(path_p, type, acl);
|
||||
free((void *)path_p);
|
||||
return ret;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func getFile(name string, t C.acl_type_t) (*ACL, error) {
|
||||
a, err := C._go_acl_get_file(C.CString(name), t)
|
||||
if errors.Is(err, syscall.ENODATA) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return newACL(a), err
|
||||
}
|
||||
|
||||
func (acl *ACL) setFile(name string, t C.acl_type_t) error {
|
||||
_, err := C._go_acl_set_file(C.CString(name), t, acl.acl)
|
||||
return err
|
||||
}
|
||||
|
||||
func newACL(a C.acl_t) *ACL {
|
||||
acl := &ACL{a}
|
||||
runtime.SetFinalizer(acl, (*ACL).free)
|
||||
return acl
|
||||
}
|
||||
|
||||
type ACL struct {
|
||||
acl C.acl_t
|
||||
}
|
||||
|
||||
func (acl *ACL) free() {
|
||||
C.acl_free(unsafe.Pointer(acl.acl))
|
||||
|
||||
// no need for a finalizer anymore
|
||||
runtime.SetFinalizer(acl, nil)
|
||||
}
|
||||
|
||||
const (
|
||||
Read = C.ACL_READ
|
||||
Write = C.ACL_WRITE
|
||||
Execute = C.ACL_EXECUTE
|
||||
|
||||
TypeDefault = C.ACL_TYPE_DEFAULT
|
||||
TypeAccess = C.ACL_TYPE_ACCESS
|
||||
|
||||
UndefinedTag = C.ACL_UNDEFINED_TAG
|
||||
UserObj = C.ACL_USER_OBJ
|
||||
User = C.ACL_USER
|
||||
GroupObj = C.ACL_GROUP_OBJ
|
||||
Group = C.ACL_GROUP
|
||||
Mask = C.ACL_MASK
|
||||
Other = C.ACL_OTHER
|
||||
)
|
||||
|
||||
type (
|
||||
Perm C.acl_perm_t
|
||||
)
|
||||
|
||||
func (acl *ACL) removeEntry(tt C.acl_tag_t, tq int) error {
|
||||
var e C.acl_entry_t
|
||||
|
||||
// get first entry
|
||||
if r, err := C.acl_get_entry(acl.acl, C.ACL_FIRST_ENTRY, &e); err != nil {
|
||||
return err
|
||||
} else if r == 0 {
|
||||
// return on acl with no entries
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
if r, err := C.acl_get_entry(acl.acl, C.ACL_NEXT_ENTRY, &e); err != nil {
|
||||
return err
|
||||
} else if r == 0 {
|
||||
// return on drained acl
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
q int
|
||||
t C.acl_tag_t
|
||||
)
|
||||
|
||||
// get current entry tag type
|
||||
if _, err := C.acl_get_tag_type(e, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get current entry qualifier
|
||||
if rq, err := C.acl_get_qualifier(e); err != nil {
|
||||
// neither ACL_USER nor ACL_GROUP
|
||||
if errors.Is(err, syscall.EINVAL) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
} else {
|
||||
q = *(*int)(rq)
|
||||
C.acl_free(rq)
|
||||
}
|
||||
|
||||
// delete on match
|
||||
if t == tt && q == tq {
|
||||
_, err := C.acl_delete_entry(acl.acl, e)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UpdatePerm(name string, uid int, perms ...Perm) error {
|
||||
// read acl from file
|
||||
a, err := getFile(name, TypeAccess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// free acl on return if get is successful
|
||||
defer a.free()
|
||||
|
||||
// remove existing entry
|
||||
if err = a.removeEntry(User, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new entry if perms are passed
|
||||
if len(perms) > 0 {
|
||||
// create new acl entry
|
||||
var e C.acl_entry_t
|
||||
if _, err = C.acl_create_entry(&a.acl, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get perm set of new entry
|
||||
var p C.acl_permset_t
|
||||
if _, err = C.acl_get_permset(e, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add target perms
|
||||
for _, perm := range perms {
|
||||
if _, err = C.acl_add_perm(p, C.acl_perm_t(perm)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set perm set to new entry
|
||||
if _, err = C.acl_set_permset(e, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set user tag to new entry
|
||||
if _, err = C.acl_set_tag_type(e, User); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set qualifier (uid) to new entry
|
||||
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// calculate mask after update
|
||||
if _, err = C.acl_calc_mask(&a.acl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write acl to file
|
||||
return a.setFile(name, TypeAccess)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/internal/system"
|
||||
)
|
||||
|
||||
type bundleInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
|
||||
// passed through to [fst.Config]
|
||||
ID string `json:"id"`
|
||||
// passed through to [fst.Config]
|
||||
AppID int `json:"app_id"`
|
||||
// passed through to [fst.Config]
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
UserNS bool `json:"userns,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
Net bool `json:"net,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
Dev bool `json:"dev,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
NoNewSession bool `json:"no_new_session,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
MapRealUID bool `json:"map_real_uid,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
Enablements system.Enablements `json:"enablements"`
|
||||
|
||||
// passed through inverted to [bwrap.SyscallPolicy]
|
||||
Devel bool `json:"devel,omitempty"`
|
||||
// passed through to [bwrap.SyscallPolicy]
|
||||
Multiarch bool `json:"multiarch,omitempty"`
|
||||
// passed through to [bwrap.SyscallPolicy]
|
||||
Bluetooth bool `json:"bluetooth,omitempty"`
|
||||
|
||||
// allow gpu access within sandbox
|
||||
GPU bool `json:"gpu"`
|
||||
// store path to nixGL mesa wrappers
|
||||
Mesa string `json:"mesa,omitempty"`
|
||||
// store path to nixGL source
|
||||
NixGL string `json:"nix_gl,omitempty"`
|
||||
// store path to activate-and-exec script
|
||||
Launcher string `json:"launcher"`
|
||||
// store path to /run/current-system
|
||||
CurrentSystem string `json:"current_system"`
|
||||
// store path to home-manager activation package
|
||||
ActivationPackage string `json:"activation_package"`
|
||||
}
|
||||
|
||||
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
|
||||
bundle := new(bundleInfo)
|
||||
if f, err := os.Open(name); err != nil {
|
||||
beforeFail()
|
||||
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,191 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func actionInstall(args []string) {
|
||||
set := flag.NewFlagSet("install", flag.ExitOnError)
|
||||
var (
|
||||
dropShellInstall bool
|
||||
dropShellActivate bool
|
||||
)
|
||||
set.BoolVar(&dropShellInstall, "si", false, "Drop to a shell on installation")
|
||||
set.BoolVar(&dropShellActivate, "sa", false, "Drop to a shell on activation")
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args)
|
||||
|
||||
args = set.Args()
|
||||
|
||||
if len(args) != 1 {
|
||||
log.Fatal("invalid argument")
|
||||
}
|
||||
pkgPath := args[0]
|
||||
if !path.IsAbs(pkgPath) {
|
||||
if dir, err := os.Getwd(); err != nil {
|
||||
log.Fatalf("cannot get current directory: %v", err)
|
||||
} else {
|
||||
pkgPath = path.Join(dir, pkgPath)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Look up paths to programs started by fpkg.
|
||||
This is done here to ease error handling as cleanup is not yet required.
|
||||
*/
|
||||
|
||||
var (
|
||||
_ = lookPath("zstd")
|
||||
tar = lookPath("tar")
|
||||
chmod = lookPath("chmod")
|
||||
rm = lookPath("rm")
|
||||
)
|
||||
|
||||
/*
|
||||
Extract package and set up for cleanup.
|
||||
*/
|
||||
|
||||
var workDir string
|
||||
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
|
||||
log.Fatalf("cannot create temporary directory: %v", err)
|
||||
} else {
|
||||
workDir = p
|
||||
}
|
||||
cleanup := func() {
|
||||
// should be faster than a native implementation
|
||||
mustRun(chmod, "-R", "+w", workDir)
|
||||
mustRun(rm, "-rf", workDir)
|
||||
}
|
||||
beforeRunFail.Store(&cleanup)
|
||||
|
||||
mustRun(tar, "-C", workDir, "-xf", pkgPath)
|
||||
|
||||
/*
|
||||
Parse bundle and app metadata, do pre-install checks.
|
||||
*/
|
||||
|
||||
bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
|
||||
pathSet := pathSetByApp(bundle.ID)
|
||||
|
||||
app := bundle
|
||||
if s, err := os.Stat(pathSet.metaPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
cleanup()
|
||||
log.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
|
||||
}
|
||||
// did not modify app, clean installation condition met later
|
||||
} else if s.IsDir() {
|
||||
cleanup()
|
||||
log.Fatalf("metadata path %q is not a file", pathSet.metaPath)
|
||||
} else {
|
||||
app = loadBundleInfo(pathSet.metaPath, cleanup)
|
||||
if app.ID != bundle.ID {
|
||||
cleanup()
|
||||
log.Fatalf("app %q claims to have identifier %q", bundle.ID, app.ID)
|
||||
}
|
||||
// sec: should verify credentials
|
||||
}
|
||||
|
||||
if app != bundle {
|
||||
// do not try to re-install
|
||||
if app.NixGL == bundle.NixGL &&
|
||||
app.CurrentSystem == bundle.CurrentSystem &&
|
||||
app.Launcher == bundle.Launcher &&
|
||||
app.ActivationPackage == bundle.ActivationPackage {
|
||||
cleanup()
|
||||
log.Printf("package %q is identical to local application %q", pkgPath, app.ID)
|
||||
internal.Exit(0)
|
||||
}
|
||||
|
||||
// AppID determines uid
|
||||
if app.AppID != bundle.AppID {
|
||||
cleanup()
|
||||
log.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
|
||||
}
|
||||
|
||||
// sec: should compare version string
|
||||
fmsg.Verbosef("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
|
||||
} else {
|
||||
fmsg.Verbosef("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
/*
|
||||
Setup steps for files owned by the target user.
|
||||
*/
|
||||
|
||||
withCacheDir("install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + fst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
"mkdir -p etc",
|
||||
"chmod -R +w etc",
|
||||
"rm -rf etc",
|
||||
"cp -dRf $BUNDLE/etc etc",
|
||||
// replace inner /nix
|
||||
"mkdir -p nix",
|
||||
"chmod -R +w nix",
|
||||
"rm -rf nix",
|
||||
"cp -dRf /nix nix",
|
||||
// copy from binary cache
|
||||
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
|
||||
// deduplicate nix store
|
||||
"nix store --offline --store $PWD optimise",
|
||||
// make cache directory world-readable for autoetc
|
||||
"chmod 0755 .",
|
||||
}, workDir, bundle, pathSet, dropShellInstall, cleanup)
|
||||
|
||||
if bundle.GPU {
|
||||
withCacheDir("mesa-wrappers", []string{
|
||||
// link nixGL mesa wrappers
|
||||
"mkdir -p nix/.nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
|
||||
}, workDir, bundle, pathSet, false, cleanup)
|
||||
}
|
||||
|
||||
/*
|
||||
Activate home-manager generation.
|
||||
*/
|
||||
|
||||
withNixDaemon("activate", []string{
|
||||
// clean up broken links
|
||||
"mkdir -p .local/state/{nix,home-manager}",
|
||||
"chmod -R +w .local/state/{nix,home-manager}",
|
||||
"rm -rf .local/state/{nix,home-manager}",
|
||||
// run activation script
|
||||
bundle.ActivationPackage + "/activate",
|
||||
}, false, func(config *fst.Config) *fst.Config { return config }, bundle, pathSet, dropShellActivate, cleanup)
|
||||
|
||||
/*
|
||||
Installation complete. Write metadata to block re-installs or downgrades.
|
||||
*/
|
||||
|
||||
// serialise metadata to ensure consistency
|
||||
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||
cleanup()
|
||||
log.Fatalf("cannot create metadata file: %v", err)
|
||||
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
|
||||
cleanup()
|
||||
log.Fatalf("cannot write metadata: %v", err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Printf("cannot close metadata file: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
|
||||
cleanup()
|
||||
log.Fatalf("cannot rename metadata file: %v", err)
|
||||
}
|
||||
|
||||
cleanup()
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
const shell = "/run/current-system/sw/bin/bash"
|
||||
|
||||
func init() {
|
||||
if err := os.Setenv("SHELL", shell); err != nil {
|
||||
log.Fatalf("cannot set $SHELL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
flagVerbose bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmsg.Prepare("fpkg")
|
||||
|
||||
flag.Parse()
|
||||
fmsg.Store(flagVerbose)
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) < 1 {
|
||||
log.Fatal("invalid argument")
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "install":
|
||||
actionInstall(args[1:])
|
||||
case "start":
|
||||
actionStart(args[1:])
|
||||
|
||||
default:
|
||||
log.Fatal("invalid argument")
|
||||
}
|
||||
|
||||
internal.Exit(0)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
var (
|
||||
dataHome string
|
||||
)
|
||||
|
||||
func init() {
|
||||
// dataHome
|
||||
if p, ok := os.LookupEnv("FORTIFY_DATA_HOME"); ok {
|
||||
dataHome = p
|
||||
} else {
|
||||
dataHome = "/var/lib/fortify/" + strconv.Itoa(os.Getuid())
|
||||
}
|
||||
}
|
||||
|
||||
func lookPath(file string) string {
|
||||
if p, err := exec.LookPath(file); err != nil {
|
||||
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
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"path"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
)
|
||||
|
||||
func actionStart(args []string) {
|
||||
set := flag.NewFlagSet("start", flag.ExitOnError)
|
||||
var (
|
||||
dropShell bool
|
||||
dropShellNixGL bool
|
||||
autoDrivers bool
|
||||
)
|
||||
set.BoolVar(&dropShell, "s", false, "Drop to a shell")
|
||||
set.BoolVar(&dropShellNixGL, "sg", false, "Drop to a shell on nixGL build")
|
||||
set.BoolVar(&autoDrivers, "autodrivers", false, "Attempt automatic opengl driver detection")
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args)
|
||||
|
||||
args = set.Args()
|
||||
|
||||
if len(args) < 1 {
|
||||
log.Fatal("invalid argument")
|
||||
}
|
||||
|
||||
/*
|
||||
Parse app metadata.
|
||||
*/
|
||||
|
||||
id := args[0]
|
||||
pathSet := pathSetByApp(id)
|
||||
app := loadBundleInfo(pathSet.metaPath, func() {})
|
||||
if app.ID != id {
|
||||
log.Fatalf("app %q claims to have identifier %q", id, app.ID)
|
||||
}
|
||||
|
||||
/*
|
||||
Prepare nixGL.
|
||||
*/
|
||||
|
||||
if app.GPU && autoDrivers {
|
||||
withNixDaemon("nix-gl", []string{
|
||||
"mkdir -p /nix/.nixGL/auto",
|
||||
"rm -rf /nix/.nixGL/auto",
|
||||
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/opengl " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + app.NixGL,
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + app.NixGL + "#nixVulkanNvidia",
|
||||
}, true, func(config *fst.Config) *fst.Config {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||
{Src: "/etc/resolv.conf"},
|
||||
{Src: "/sys/block"},
|
||||
{Src: "/sys/bus"},
|
||||
{Src: "/sys/class"},
|
||||
{Src: "/sys/dev"},
|
||||
{Src: "/sys/devices"},
|
||||
}...)
|
||||
appendGPUFilesystem(config)
|
||||
return config
|
||||
}, app, pathSet, dropShellNixGL, func() {})
|
||||
}
|
||||
|
||||
/*
|
||||
Create app configuration.
|
||||
*/
|
||||
|
||||
command := make([]string, 1, len(args))
|
||||
if !dropShell {
|
||||
command[0] = app.Launcher
|
||||
} else {
|
||||
command[0] = shell
|
||||
}
|
||||
command = append(command, args[1:]...)
|
||||
|
||||
config := &fst.Config{
|
||||
ID: app.ID,
|
||||
Command: command,
|
||||
Confinement: fst.ConfinementConfig{
|
||||
AppID: app.AppID,
|
||||
Groups: app.Groups,
|
||||
Username: "fortify",
|
||||
Inner: path.Join("/data/data", app.ID),
|
||||
Outer: pathSet.homeDir,
|
||||
Sandbox: &fst.SandboxConfig{
|
||||
Hostname: formatHostname(app.Name),
|
||||
UserNS: app.UserNS,
|
||||
Net: app.Net,
|
||||
Dev: app.Dev,
|
||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
|
||||
NoNewSession: app.NoNewSession || dropShell,
|
||||
MapRealUID: app.MapRealUID,
|
||||
DirectWayland: app.DirectWayland,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
|
||||
{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
|
||||
{Src: "/etc/resolv.conf"},
|
||||
{Src: "/sys/block"},
|
||||
{Src: "/sys/bus"},
|
||||
{Src: "/sys/class"},
|
||||
{Src: "/sys/dev"},
|
||||
{Src: "/sys/devices"},
|
||||
},
|
||||
Link: [][2]string{
|
||||
{app.CurrentSystem, "/run/current-system"},
|
||||
{"/run/current-system/sw/bin", "/bin"},
|
||||
{"/run/current-system/sw/bin", "/usr/bin"},
|
||||
},
|
||||
Etc: path.Join(pathSet.cacheDir, "etc"),
|
||||
AutoEtc: true,
|
||||
},
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
SystemBus: app.SystemBus,
|
||||
SessionBus: app.SessionBus,
|
||||
Enablements: app.Enablements,
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Expose GPU devices.
|
||||
*/
|
||||
|
||||
if app.GPU {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
|
||||
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
|
||||
appendGPUFilesystem(config)
|
||||
}
|
||||
|
||||
/*
|
||||
Spawn app.
|
||||
*/
|
||||
|
||||
fortifyApp(config, func() {})
|
||||
internal.Exit(0)
|
||||
}
|
||||
|
||||
func appendGPUFilesystem(config *fst.Config) {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
|
||||
{Src: "/dev/dri", Device: true},
|
||||
// mali
|
||||
{Src: "/dev/mali", Device: true},
|
||||
{Src: "/dev/mali0", Device: true},
|
||||
{Src: "/dev/umplock", Device: true},
|
||||
// nvidia
|
||||
{Src: "/dev/nvidiactl", Device: true},
|
||||
{Src: "/dev/nvidia-modeset", Device: true},
|
||||
// nvidia OpenCL/CUDA
|
||||
{Src: "/dev/nvidia-uvm", Device: true},
|
||||
{Src: "/dev/nvidia-uvm-tools", Device: true},
|
||||
|
||||
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
|
||||
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
|
||||
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
|
||||
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
|
||||
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
|
||||
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
|
||||
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
|
||||
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
|
||||
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
|
||||
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
|
||||
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
|
||||
}...)
|
||||
}
|
||||
101
cmd/fpkg/with.go
101
cmd/fpkg/with.go
@@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
)
|
||||
|
||||
func withNixDaemon(
|
||||
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
|
||||
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||
) {
|
||||
fortifyAppDropShell(updateConfig(&fst.Config{
|
||||
ID: app.ID,
|
||||
Command: []string{shell, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
|
||||
// start nix-daemon
|
||||
"nix-daemon --store / & " +
|
||||
// wait for socket to appear
|
||||
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
|
||||
// create directory so nix stops complaining
|
||||
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
|
||||
strings.Join(command, " && ") +
|
||||
// terminate nix-daemon
|
||||
" && pkill nix-daemon",
|
||||
},
|
||||
Confinement: fst.ConfinementConfig{
|
||||
AppID: app.AppID,
|
||||
Username: "fortify",
|
||||
Inner: path.Join("/data/data", app.ID),
|
||||
Outer: pathSet.homeDir,
|
||||
Sandbox: &fst.SandboxConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
UserNS: true, // nix sandbox requires userns
|
||||
Net: net,
|
||||
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
|
||||
NoNewSession: dropShell,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
|
||||
},
|
||||
Link: [][2]string{
|
||||
{app.CurrentSystem, "/run/current-system"},
|
||||
{"/run/current-system/sw/bin", "/bin"},
|
||||
{"/run/current-system/sw/bin", "/usr/bin"},
|
||||
},
|
||||
Etc: path.Join(pathSet.cacheDir, "etc"),
|
||||
AutoEtc: true,
|
||||
},
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
},
|
||||
}), dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
fortifyAppDropShell(&fst.Config{
|
||||
ID: app.ID,
|
||||
Command: []string{shell, "-lc", strings.Join(command, " && ")},
|
||||
Confinement: fst.ConfinementConfig{
|
||||
AppID: app.AppID,
|
||||
Username: "nixos",
|
||||
Inner: path.Join("/data/data", app.ID, "cache"),
|
||||
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim
|
||||
Sandbox: &fst.SandboxConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
|
||||
NoNewSession: dropShell,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
|
||||
{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
|
||||
},
|
||||
Link: [][2]string{
|
||||
{app.CurrentSystem, "/run/current-system"},
|
||||
{"/run/current-system/sw/bin", "/bin"},
|
||||
{"/run/current-system/sw/bin", "/usr/bin"},
|
||||
},
|
||||
Etc: path.Join(workDir, "etc"),
|
||||
AutoEtc: true,
|
||||
},
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
{Path: workDir, Execute: true},
|
||||
},
|
||||
},
|
||||
}, dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
|
||||
if dropShell {
|
||||
config.Command = []string{shell, "-l"}
|
||||
fortifyApp(config, beforeFail)
|
||||
beforeFail()
|
||||
internal.Exit(0)
|
||||
}
|
||||
fortifyApp(config, beforeFail)
|
||||
}
|
||||
258
cmd/hakurei/command.go
Normal file
258
cmd/hakurei/command.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/instance"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func buildCommand(out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
|
||||
|
||||
c.Command("app", "Load app from configuration file", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("app requires at least 1 argument")
|
||||
}
|
||||
|
||||
// config extraArgs...
|
||||
config := tryPath(args[0])
|
||||
config.Args = append(config.Args, args[1:]...)
|
||||
|
||||
runApp(config)
|
||||
panic("unreachable")
|
||||
})
|
||||
|
||||
{
|
||||
var (
|
||||
dbusConfigSession string
|
||||
dbusConfigSystem string
|
||||
mpris bool
|
||||
dbusVerbose bool
|
||||
|
||||
fid string
|
||||
aid int
|
||||
groups command.RepeatableFlag
|
||||
homeDir string
|
||||
userName string
|
||||
|
||||
wayland, x11, dBus, pulse bool
|
||||
)
|
||||
|
||||
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||
// initialise config from flags
|
||||
config := &hst.Config{
|
||||
ID: fid,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
if aid < 0 || aid > 9999 {
|
||||
log.Fatalf("aid %d out of range", aid)
|
||||
}
|
||||
|
||||
// resolve home/username from os when flag is unset
|
||||
var (
|
||||
passwd *user.User
|
||||
passwdOnce sync.Once
|
||||
passwdFunc = func() {
|
||||
var us string
|
||||
if uid, err := std.Uid(aid); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
us = strconv.Itoa(uid)
|
||||
}
|
||||
|
||||
if u, err := user.LookupId(us); err != nil {
|
||||
hlog.Verbosef("cannot look up uid %s", us)
|
||||
passwd = &user.User{
|
||||
Uid: us,
|
||||
Gid: us,
|
||||
Username: "chronos",
|
||||
Name: "Hakurei Permissive Default",
|
||||
HomeDir: "/var/empty",
|
||||
}
|
||||
} else {
|
||||
passwd = u
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if homeDir == "os" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
homeDir = passwd.HomeDir
|
||||
}
|
||||
|
||||
if userName == "chronos" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
userName = passwd.Username
|
||||
}
|
||||
|
||||
config.Identity = aid
|
||||
config.Groups = groups
|
||||
config.Data = homeDir
|
||||
config.Username = userName
|
||||
|
||||
if wayland {
|
||||
config.Enablements |= system.EWayland
|
||||
}
|
||||
if x11 {
|
||||
config.Enablements |= system.EX11
|
||||
}
|
||||
if dBus {
|
||||
config.Enablements |= system.EDBus
|
||||
}
|
||||
if pulse {
|
||||
config.Enablements |= system.EPulse
|
||||
}
|
||||
|
||||
// parse D-Bus config file from flags if applicable
|
||||
if dBus {
|
||||
if dbusConfigSession == "builtin" {
|
||||
config.SessionBus = dbus.NewConfig(fid, true, mpris)
|
||||
} else {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
||||
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
||||
} else {
|
||||
config.SessionBus = conf
|
||||
}
|
||||
}
|
||||
|
||||
// system bus proxy is optional
|
||||
if dbusConfigSystem != "nil" {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
||||
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
||||
} else {
|
||||
config.SystemBus = conf
|
||||
}
|
||||
}
|
||||
|
||||
// override log from configuration
|
||||
if dbusVerbose {
|
||||
config.SessionBus.Log = true
|
||||
config.SystemBus.Log = true
|
||||
}
|
||||
}
|
||||
|
||||
// invoke app
|
||||
runApp(config)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
||||
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||
"Path to system bus proxy config file, or \"nil\" to disable").
|
||||
Flag(&mpris, "mpris", command.BoolFlag(false),
|
||||
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
||||
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
|
||||
"Force buffered logging in the D-Bus proxy").
|
||||
Flag(&fid, "id", command.StringFlag(""),
|
||||
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
||||
Flag(&aid, "a", command.IntFlag(0),
|
||||
"Application identity").
|
||||
Flag(nil, "g", &groups,
|
||||
"Groups inherited by all container processes").
|
||||
Flag(&homeDir, "d", command.StringFlag("os"),
|
||||
"Container home directory").
|
||||
Flag(&userName, "u", command.StringFlag("chronos"),
|
||||
"Passwd user name within sandbox").
|
||||
Flag(&wayland, "wayland", command.BoolFlag(false),
|
||||
"Enable connection to Wayland via security-context-v1").
|
||||
Flag(&x11, "X", command.BoolFlag(false),
|
||||
"Enable direct connection to X11").
|
||||
Flag(&dBus, "dbus", command.BoolFlag(false),
|
||||
"Enable proxied connection to D-Bus").
|
||||
Flag(&pulse, "pulse", command.BoolFlag(false),
|
||||
"Enable direct connection to PulseAudio")
|
||||
}
|
||||
|
||||
var showFlagShort bool
|
||||
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, showFlagShort, flagJSON)
|
||||
|
||||
case 1: // instance
|
||||
name := args[0]
|
||||
config, entry := tryShort(name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
}
|
||||
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
|
||||
|
||||
default:
|
||||
log.Fatal("show requires 1 argument")
|
||||
}
|
||||
return errSuccess
|
||||
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||
|
||||
var psFlagShort bool
|
||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
|
||||
c.Command("version", "Display version information", func(args []string) error {
|
||||
fmt.Println(internal.Version())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("license", "Show full license text", func(args []string) error {
|
||||
fmt.Println(license)
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("template", "Produce a config template", func(args []string) error {
|
||||
printJSON(os.Stdout, false, hst.Template())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("help", "Show this help message", func([]string) error {
|
||||
c.PrintHelp()
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func runApp(config *hst.Config) {
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
a := instance.MustNew(instance.ISetuid, ctx, std)
|
||||
|
||||
rs := new(app.RunState)
|
||||
if sa, err := a.Seal(config); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot seal app:")
|
||||
internal.Exit(1)
|
||||
} else {
|
||||
internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs)))
|
||||
}
|
||||
|
||||
*(*int)(nil) = 0 // not reached
|
||||
}
|
||||
81
cmd/hakurei/command_test.go
Normal file
81
cmd/hakurei/command_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"main", []string{}, `
|
||||
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
app Load app from configuration file
|
||||
run Configure and start a permissive default sandbox
|
||||
show Show live or local app configuration
|
||||
ps List active instances
|
||||
version Display version information
|
||||
license Show full license text
|
||||
template Produce a config template
|
||||
help Show this help message
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
"run", []string{"run", "-h"}, `
|
||||
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
||||
|
||||
Flags:
|
||||
-X Enable direct connection to X11
|
||||
-a int
|
||||
Application identity
|
||||
-d string
|
||||
Container home directory (default "os")
|
||||
-dbus
|
||||
Enable proxied connection to D-Bus
|
||||
-dbus-config string
|
||||
Path to session bus proxy config file, or "builtin" for defaults (default "builtin")
|
||||
-dbus-log
|
||||
Force buffered logging in the D-Bus proxy
|
||||
-dbus-system string
|
||||
Path to system bus proxy config file, or "nil" to disable (default "nil")
|
||||
-g value
|
||||
Groups inherited by all container processes
|
||||
-id string
|
||||
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||
-mpris
|
||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||
-pulse
|
||||
Enable direct connection to PulseAudio
|
||||
-u string
|
||||
Passwd user name within sandbox (default "chronos")
|
||||
-wayland
|
||||
Enable connection to Wayland via security-context-v1
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := new(bytes.Buffer)
|
||||
c := buildCommand(out)
|
||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||
t.Errorf("Parse: error = %v; want %v",
|
||||
err, command.ErrHelp)
|
||||
}
|
||||
if got := out.String(); got != tc.want {
|
||||
t.Errorf("Parse: %s want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
49
cmd/hakurei/internal/app/app.go
Normal file
49
cmd/hakurei/internal/app/app.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Package app defines the generic [App] interface.
|
||||
package app
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
type App interface {
|
||||
// ID returns a copy of [ID] held by App.
|
||||
ID() ID
|
||||
|
||||
// Seal determines the outcome of config as a [SealedApp].
|
||||
// The value of config might be overwritten and must not be used again.
|
||||
Seal(config *hst.Config) (SealedApp, error)
|
||||
|
||||
String() string
|
||||
}
|
||||
|
||||
type SealedApp interface {
|
||||
// Run commits sealed system setup and starts the app process.
|
||||
Run(rs *RunState) error
|
||||
}
|
||||
|
||||
// RunState stores the outcome of a call to [SealedApp.Run].
|
||||
type RunState struct {
|
||||
// Time is the exact point in time where the process was created.
|
||||
// Location must be set to UTC.
|
||||
//
|
||||
// Time is nil if no process was ever created.
|
||||
Time *time.Time
|
||||
// RevertErr is stored by the deferred revert call.
|
||||
RevertErr error
|
||||
// WaitErr is the generic error value created by the standard library.
|
||||
WaitErr error
|
||||
|
||||
syscall.WaitStatus
|
||||
}
|
||||
|
||||
// SetStart stores the current time in [RunState] once.
|
||||
func (rs *RunState) SetStart() {
|
||||
if rs.Time != nil {
|
||||
panic("attempted to store time twice")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
rs.Time = &now
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package fst
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -1,22 +1,22 @@
|
||||
package fst_test
|
||||
package app_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
)
|
||||
|
||||
func TestParseAppID(t *testing.T) {
|
||||
t.Run("bad length", func(t *testing.T) {
|
||||
if err := fst.ParseAppID(new(fst.ID), "meow"); !errors.Is(err, fst.ErrInvalidLength) {
|
||||
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, fst.ErrInvalidLength)
|
||||
if err := ParseAppID(new(ID), "meow"); !errors.Is(err, ErrInvalidLength) {
|
||||
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, ErrInvalidLength)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bad byte", func(t *testing.T) {
|
||||
wantErr := "invalid char '\\n' at byte 15"
|
||||
if err := fst.ParseAppID(new(fst.ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr {
|
||||
if err := ParseAppID(new(ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr {
|
||||
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
@@ -30,30 +30,30 @@ func TestParseAppID(t *testing.T) {
|
||||
|
||||
func FuzzParseAppID(f *testing.F) {
|
||||
for i := 0; i < 16; i++ {
|
||||
id := new(fst.ID)
|
||||
if err := fst.NewAppID(id); err != nil {
|
||||
id := new(ID)
|
||||
if err := NewAppID(id); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15])
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) {
|
||||
testParseAppID(t, &fst.ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15})
|
||||
testParseAppID(t, &ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15})
|
||||
})
|
||||
}
|
||||
|
||||
func testParseAppIDWithRandom(t *testing.T) {
|
||||
id := new(fst.ID)
|
||||
if err := fst.NewAppID(id); err != nil {
|
||||
id := new(ID)
|
||||
if err := NewAppID(id); err != nil {
|
||||
t.Fatalf("cannot generate app ID: %v", err)
|
||||
}
|
||||
testParseAppID(t, id)
|
||||
}
|
||||
|
||||
func testParseAppID(t *testing.T, id *fst.ID) {
|
||||
func testParseAppID(t *testing.T, id *ID) {
|
||||
s := id.String()
|
||||
got := new(fst.ID)
|
||||
if err := fst.ParseAppID(got, s); err != nil {
|
||||
got := new(ID)
|
||||
if err := ParseAppID(got, s); err != nil {
|
||||
t.Fatalf("cannot parse app ID: %v", err)
|
||||
}
|
||||
|
||||
192
cmd/hakurei/internal/app/instance/common/container.go
Normal file
192
cmd/hakurei/internal/app/instance/common/container.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
// in practice there should be less than 30 entries added by the runtime;
|
||||
// allocating slightly more as a margin for future expansion
|
||||
const preallocateOpsCount = 1 << 5
|
||||
|
||||
// NewContainer initialises [sandbox.Params] via [hst.ContainerConfig].
|
||||
// Note that remaining container setup must be queued by the caller.
|
||||
func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*container.Params, map[string]string, error) {
|
||||
if s == nil {
|
||||
return nil, nil, syscall.EBADE
|
||||
}
|
||||
|
||||
params := &container.Params{
|
||||
Hostname: s.Hostname,
|
||||
SeccompFlags: s.SeccompFlags,
|
||||
SeccompPresets: s.SeccompPresets,
|
||||
RetainSession: s.Tty,
|
||||
HostNet: s.Net,
|
||||
}
|
||||
|
||||
{
|
||||
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover))
|
||||
params.Ops = &ops
|
||||
}
|
||||
|
||||
if s.Multiarch {
|
||||
params.SeccompFlags |= seccomp.AllowMultiarch
|
||||
}
|
||||
|
||||
if !s.SeccompCompat {
|
||||
params.SeccompPresets |= seccomp.PresetExt
|
||||
}
|
||||
if !s.Devel {
|
||||
params.SeccompPresets |= seccomp.PresetDenyDevel
|
||||
}
|
||||
if !s.Userns {
|
||||
params.SeccompPresets |= seccomp.PresetDenyNS
|
||||
}
|
||||
if !s.Tty {
|
||||
params.SeccompPresets |= seccomp.PresetDenyTTY
|
||||
}
|
||||
|
||||
if s.MapRealUID {
|
||||
/* some programs fail to connect to dbus session running as a different uid
|
||||
so this workaround is introduced to map priv-side caller uid in container */
|
||||
params.Uid = os.Getuid()
|
||||
*uid = params.Uid
|
||||
params.Gid = os.Getgid()
|
||||
*gid = params.Gid
|
||||
} else {
|
||||
*uid = container.OverflowUid()
|
||||
*gid = container.OverflowGid()
|
||||
}
|
||||
|
||||
params.
|
||||
Proc("/proc").
|
||||
Tmpfs(hst.Tmp, 1<<12, 0755)
|
||||
|
||||
if !s.Device {
|
||||
params.Dev("/dev").Mqueue("/dev/mqueue")
|
||||
} else {
|
||||
params.Bind("/dev", "/dev", container.BindWritable|container.BindDevice)
|
||||
}
|
||||
|
||||
/* retrieve paths and hide them if they're made available in the sandbox;
|
||||
this feature tries to improve user experience of permissive defaults, and
|
||||
to warn about issues in custom configuration; it is NOT a security feature
|
||||
and should not be treated as such, ALWAYS be careful with what you bind */
|
||||
var hidePaths []string
|
||||
sc := os.Paths()
|
||||
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
|
||||
_, systemBusAddr := dbus.Address()
|
||||
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
||||
return nil, nil, err
|
||||
} else {
|
||||
// there is usually only one, do not preallocate
|
||||
for _, entry := range entries {
|
||||
if entry.Method != "unix" {
|
||||
continue
|
||||
}
|
||||
for _, pair := range entry.Values {
|
||||
if pair[0] == "path" {
|
||||
if path.IsAbs(pair[1]) {
|
||||
// get parent dir of socket
|
||||
dir := path.Dir(pair[1])
|
||||
if dir == "." || dir == "/" {
|
||||
os.Printf("dbus socket %q is in an unusual location", pair[1])
|
||||
}
|
||||
hidePaths = append(hidePaths, dir)
|
||||
} else {
|
||||
os.Printf("dbus socket %q is not absolute", pair[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hidePathMatch := make([]bool, len(hidePaths))
|
||||
for i := range hidePaths {
|
||||
if err := evalSymlinks(os, &hidePaths[i]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range s.Filesystem {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !path.IsAbs(c.Src) {
|
||||
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
|
||||
}
|
||||
|
||||
dest := c.Dst
|
||||
if c.Dst == "" {
|
||||
dest = c.Src
|
||||
} else if !path.IsAbs(dest) {
|
||||
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
|
||||
}
|
||||
|
||||
srcH := c.Src
|
||||
if err := evalSymlinks(os, &srcH); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for i := range hidePaths {
|
||||
// skip matched entries
|
||||
if hidePathMatch[i] {
|
||||
continue
|
||||
}
|
||||
|
||||
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
|
||||
return nil, nil, err
|
||||
} else if ok {
|
||||
hidePathMatch[i] = true
|
||||
os.Printf("hiding paths from %q", c.Src)
|
||||
}
|
||||
}
|
||||
|
||||
var flags int
|
||||
if c.Write {
|
||||
flags |= container.BindWritable
|
||||
}
|
||||
if c.Device {
|
||||
flags |= container.BindDevice | container.BindWritable
|
||||
}
|
||||
if !c.Must {
|
||||
flags |= container.BindOptional
|
||||
}
|
||||
params.Bind(c.Src, dest, flags)
|
||||
}
|
||||
|
||||
// cover matched paths
|
||||
for i, ok := range hidePathMatch {
|
||||
if ok {
|
||||
params.Tmpfs(hidePaths[i], 1<<13, 0755)
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range s.Link {
|
||||
params.Link(l[0], l[1])
|
||||
}
|
||||
|
||||
return params, maps.Clone(s.Env), nil
|
||||
}
|
||||
|
||||
func evalSymlinks(os sys.State, v *string) error {
|
||||
if p, err := os.EvalSymlinks(*v); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
os.Printf("path %q does not yet exist", *v)
|
||||
} else {
|
||||
*v = p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package fst
|
||||
package common
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
@@ -1,4 +1,4 @@
|
||||
package fst
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
17
cmd/hakurei/internal/app/instance/errors.go
Normal file
17
cmd/hakurei/internal/app/instance/errors.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
|
||||
)
|
||||
|
||||
func PrintRunStateErr(whence int, rs *app.RunState, runErr error) (code int) {
|
||||
switch whence {
|
||||
case ISetuid:
|
||||
return setuid.PrintRunStateErr(rs, runErr)
|
||||
default:
|
||||
panic(syscall.EINVAL)
|
||||
}
|
||||
}
|
||||
33
cmd/hakurei/internal/app/instance/new.go
Normal file
33
cmd/hakurei/internal/app/instance/new.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Package instance exposes cross-package implementation details and provides constructors for builtin implementations.
|
||||
package instance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
|
||||
"hakurei.app/internal/sys"
|
||||
)
|
||||
|
||||
const (
|
||||
ISetuid = iota
|
||||
)
|
||||
|
||||
func New(whence int, ctx context.Context, os sys.State) (app.App, error) {
|
||||
switch whence {
|
||||
case ISetuid:
|
||||
return setuid.New(ctx, os)
|
||||
default:
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
}
|
||||
|
||||
func MustNew(whence int, ctx context.Context, os sys.State) app.App {
|
||||
a, err := New(whence, ctx, os)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot create app: %v", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
6
cmd/hakurei/internal/app/instance/shim.go
Normal file
6
cmd/hakurei/internal/app/instance/shim.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package instance
|
||||
|
||||
import "hakurei.app/cmd/hakurei/internal/app/internal/setuid"
|
||||
|
||||
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
|
||||
func ShimMain() { setuid.ShimMain() }
|
||||
74
cmd/hakurei/internal/app/internal/setuid/app.go
Normal file
74
cmd/hakurei/internal/app/internal/setuid/app.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package setuid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
)
|
||||
|
||||
func New(ctx context.Context, os sys.State) (App, error) {
|
||||
a := new(app)
|
||||
a.sys = os
|
||||
a.ctx = ctx
|
||||
|
||||
id := new(ID)
|
||||
err := NewAppID(id)
|
||||
a.id = newID(id)
|
||||
|
||||
return a, err
|
||||
}
|
||||
|
||||
type app struct {
|
||||
id *stringPair[ID]
|
||||
sys sys.State
|
||||
ctx context.Context
|
||||
|
||||
*outcome
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (a *app) ID() ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
|
||||
|
||||
func (a *app) String() string {
|
||||
if a == nil {
|
||||
return "(invalid app)"
|
||||
}
|
||||
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
|
||||
if a.outcome != nil {
|
||||
if a.outcome.user.uid == nil {
|
||||
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
|
||||
}
|
||||
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("(unsealed app %s)", a.id)
|
||||
}
|
||||
|
||||
func (a *app) Seal(config *hst.Config) (SealedApp, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.outcome != nil {
|
||||
panic("app sealed twice")
|
||||
}
|
||||
if config == nil {
|
||||
return nil, hlog.WrapErr(ErrConfig,
|
||||
"attempted to seal app with nil config")
|
||||
}
|
||||
|
||||
seal := new(outcome)
|
||||
seal.id = a.id
|
||||
err := seal.finalise(a.ctx, a.sys, config)
|
||||
if err == nil {
|
||||
a.outcome = seal
|
||||
}
|
||||
return seal, err
|
||||
}
|
||||
149
cmd/hakurei/internal/app/internal/setuid/app_nixos_test.go
Normal file
149
cmd/hakurei/internal/app/internal/setuid/app_nixos_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package setuid_test
|
||||
|
||||
import (
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
var testCasesNixos = []sealTestCase{
|
||||
{
|
||||
"nixos chromium direct wayland", new(stubNixOS),
|
||||
&hst.Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
|
||||
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Userns: true, Net: true, MapRealUID: true, Env: nil, AutoEtc: true,
|
||||
Filesystem: []*hst.FilesystemConfig{
|
||||
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
|
||||
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
|
||||
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
|
||||
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
|
||||
},
|
||||
Cover: []string{"/var/run/nscd"},
|
||||
},
|
||||
SystemBus: &dbus.Config{
|
||||
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
|
||||
Filter: true,
|
||||
},
|
||||
SessionBus: &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5", "org.kde.kwalletd6",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{}, Broadcast: map[string]string{},
|
||||
Filter: true,
|
||||
},
|
||||
DirectWayland: true,
|
||||
|
||||
Username: "u0_a1",
|
||||
Data: "/var/lib/persist/module/hakurei/0/1",
|
||||
Identity: 1, Groups: []string{},
|
||||
},
|
||||
app.ID{
|
||||
0x8e, 0x2c, 0x76, 0xb0,
|
||||
0x66, 0xda, 0xbe, 0x57,
|
||||
0x4c, 0xf0, 0x73, 0xbd,
|
||||
0xb4, 0x6e, 0xb5, 0xc1,
|
||||
},
|
||||
system.New(1000001).
|
||||
Ensure("/tmp/hakurei.1971", 0711).
|
||||
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
|
||||
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
|
||||
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
|
||||
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
|
||||
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
||||
Ephemeral(system.Process, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
|
||||
MustProxyDBus("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5", "org.kde.kwalletd6",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{}, Broadcast: map[string]string{},
|
||||
Filter: true,
|
||||
}, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower",
|
||||
},
|
||||
Filter: true,
|
||||
}).
|
||||
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
|
||||
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
|
||||
&container.Params{
|
||||
Uid: 1971,
|
||||
Gid: 100,
|
||||
Dir: "/var/lib/persist/module/hakurei/0/1",
|
||||
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
|
||||
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
|
||||
Env: []string{
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
|
||||
"HOME=/var/lib/persist/module/hakurei/0/1",
|
||||
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=u0_a1",
|
||||
"WAYLAND_DISPLAY=wayland-0",
|
||||
"XDG_RUNTIME_DIR=/run/user/1971",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=tty",
|
||||
},
|
||||
Ops: new(container.Ops).
|
||||
Proc("/proc").
|
||||
Tmpfs(hst.Tmp, 4096, 0755).
|
||||
Dev("/dev").Mqueue("/dev/mqueue").
|
||||
Bind("/bin", "/bin", 0).
|
||||
Bind("/usr/bin", "/usr/bin", 0).
|
||||
Bind("/nix/store", "/nix/store", 0).
|
||||
Bind("/run/current-system", "/run/current-system", 0).
|
||||
Bind("/sys/block", "/sys/block", container.BindOptional).
|
||||
Bind("/sys/bus", "/sys/bus", container.BindOptional).
|
||||
Bind("/sys/class", "/sys/class", container.BindOptional).
|
||||
Bind("/sys/dev", "/sys/dev", container.BindOptional).
|
||||
Bind("/sys/devices", "/sys/devices", container.BindOptional).
|
||||
Bind("/run/opengl-driver", "/run/opengl-driver", 0).
|
||||
Bind("/dev/dri", "/dev/dri", container.BindDevice|container.BindWritable|container.BindOptional).
|
||||
Etc("/etc", "8e2c76b066dabe574cf073bdb46eb5c1").
|
||||
Tmpfs("/run/user", 4096, 0755).
|
||||
Bind("/tmp/hakurei.1971/runtime/1", "/run/user/1971", container.BindWritable).
|
||||
Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp", container.BindWritable).
|
||||
Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", container.BindWritable).
|
||||
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
|
||||
Place("/etc/group", []byte("hakurei:x:100:\n")).
|
||||
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
|
||||
Bind("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
|
||||
Place(hst.Tmp+"/pulse-cookie", nil).
|
||||
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
|
||||
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
|
||||
Tmpfs("/var/run/nscd", 8192, 0755),
|
||||
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
225
cmd/hakurei/internal/app/internal/setuid/app_pd_test.go
Normal file
225
cmd/hakurei/internal/app/internal/setuid/app_pd_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package setuid_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
var testCasesPd = []sealTestCase{
|
||||
{
|
||||
"nixos permissive defaults no enablements", new(stubNixOS),
|
||||
&hst.Config{Username: "chronos", Data: "/home/chronos"},
|
||||
app.ID{
|
||||
0x4a, 0x45, 0x0b, 0x65,
|
||||
0x96, 0xd7, 0xbc, 0x15,
|
||||
0xbd, 0x01, 0x78, 0x0e,
|
||||
0xb9, 0xa6, 0x07, 0xac,
|
||||
},
|
||||
system.New(1000000).
|
||||
Ensure("/tmp/hakurei.1971", 0711).
|
||||
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
|
||||
&container.Params{
|
||||
Dir: "/home/chronos",
|
||||
Path: "/run/current-system/sw/bin/zsh",
|
||||
Args: []string{"/run/current-system/sw/bin/zsh"},
|
||||
Env: []string{
|
||||
"HOME=/home/chronos",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=tty",
|
||||
},
|
||||
Ops: new(container.Ops).
|
||||
Proc("/proc").
|
||||
Tmpfs(hst.Tmp, 4096, 0755).
|
||||
Dev("/dev").Mqueue("/dev/mqueue").
|
||||
Bind("/bin", "/bin", container.BindWritable).
|
||||
Bind("/boot", "/boot", container.BindWritable).
|
||||
Bind("/home", "/home", container.BindWritable).
|
||||
Bind("/lib", "/lib", container.BindWritable).
|
||||
Bind("/lib64", "/lib64", container.BindWritable).
|
||||
Bind("/nix", "/nix", container.BindWritable).
|
||||
Bind("/root", "/root", container.BindWritable).
|
||||
Bind("/run", "/run", container.BindWritable).
|
||||
Bind("/srv", "/srv", container.BindWritable).
|
||||
Bind("/sys", "/sys", container.BindWritable).
|
||||
Bind("/usr", "/usr", container.BindWritable).
|
||||
Bind("/var", "/var", container.BindWritable).
|
||||
Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional).
|
||||
Tmpfs("/run/user/1971", 8192, 0755).
|
||||
Tmpfs("/run/dbus", 8192, 0755).
|
||||
Etc("/etc", "4a450b6596d7bc15bd01780eb9a607ac").
|
||||
Tmpfs("/run/user", 4096, 0755).
|
||||
Bind("/tmp/hakurei.1971/runtime/0", "/run/user/65534", container.BindWritable).
|
||||
Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp", container.BindWritable).
|
||||
Bind("/home/chronos", "/home/chronos", container.BindWritable).
|
||||
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
Place("/etc/group", []byte("hakurei:x:65534:\n")).
|
||||
Tmpfs("/var/run/nscd", 8192, 0755),
|
||||
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
RetainSession: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"nixos permissive defaults chromium", new(stubNixOS),
|
||||
&hst.Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
Args: []string{"zsh", "-c", "exec chromium "},
|
||||
Identity: 9,
|
||||
Groups: []string{"video"},
|
||||
Username: "chronos",
|
||||
Data: "/home/chronos",
|
||||
SessionBus: &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{
|
||||
"org.freedesktop.portal.*": "*",
|
||||
},
|
||||
Broadcast: map[string]string{
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
SystemBus: &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower",
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
||||
},
|
||||
app.ID{
|
||||
0xeb, 0xf0, 0x83, 0xd1,
|
||||
0xb1, 0x75, 0x91, 0x17,
|
||||
0x82, 0xd4, 0x13, 0x36,
|
||||
0x9b, 0x64, 0xce, 0x7c,
|
||||
},
|
||||
system.New(1000009).
|
||||
Ensure("/tmp/hakurei.1971", 0711).
|
||||
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/9", acl.Read, acl.Write, acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c", 0711).
|
||||
Wayland(new(*os.File), "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
|
||||
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
Ephemeral(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", acl.Execute).
|
||||
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse").
|
||||
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
||||
MustProxyDBus("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{
|
||||
"org.freedesktop.portal.*": "*",
|
||||
},
|
||||
Broadcast: map[string]string{
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
|
||||
},
|
||||
Filter: true,
|
||||
}, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower",
|
||||
},
|
||||
Filter: true,
|
||||
}).
|
||||
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
|
||||
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
|
||||
&container.Params{
|
||||
Dir: "/home/chronos",
|
||||
Path: "/run/current-system/sw/bin/zsh",
|
||||
Args: []string{"zsh", "-c", "exec chromium "},
|
||||
Env: []string{
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
|
||||
"HOME=/home/chronos",
|
||||
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
"WAYLAND_DISPLAY=wayland-0",
|
||||
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=tty",
|
||||
},
|
||||
Ops: new(container.Ops).
|
||||
Proc("/proc").
|
||||
Tmpfs(hst.Tmp, 4096, 0755).
|
||||
Dev("/dev").Mqueue("/dev/mqueue").
|
||||
Bind("/bin", "/bin", container.BindWritable).
|
||||
Bind("/boot", "/boot", container.BindWritable).
|
||||
Bind("/home", "/home", container.BindWritable).
|
||||
Bind("/lib", "/lib", container.BindWritable).
|
||||
Bind("/lib64", "/lib64", container.BindWritable).
|
||||
Bind("/nix", "/nix", container.BindWritable).
|
||||
Bind("/root", "/root", container.BindWritable).
|
||||
Bind("/run", "/run", container.BindWritable).
|
||||
Bind("/srv", "/srv", container.BindWritable).
|
||||
Bind("/sys", "/sys", container.BindWritable).
|
||||
Bind("/usr", "/usr", container.BindWritable).
|
||||
Bind("/var", "/var", container.BindWritable).
|
||||
Bind("/dev/dri", "/dev/dri", container.BindWritable|container.BindDevice|container.BindOptional).
|
||||
Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional).
|
||||
Tmpfs("/run/user/1971", 8192, 0755).
|
||||
Tmpfs("/run/dbus", 8192, 0755).
|
||||
Etc("/etc", "ebf083d1b175911782d413369b64ce7c").
|
||||
Tmpfs("/run/user", 4096, 0755).
|
||||
Bind("/tmp/hakurei.1971/runtime/9", "/run/user/65534", container.BindWritable).
|
||||
Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp", container.BindWritable).
|
||||
Bind("/home/chronos", "/home/chronos", container.BindWritable).
|
||||
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
Place("/etc/group", []byte("hakurei:x:65534:\n")).
|
||||
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0).
|
||||
Bind("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
|
||||
Place(hst.Tmp+"/pulse-cookie", nil).
|
||||
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
|
||||
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
|
||||
Tmpfs("/var/run/nscd", 8192, 0755),
|
||||
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
RetainSession: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package app_test
|
||||
package setuid_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os/user"
|
||||
"strconv"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/linux"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
// fs methods are not implemented using a real FS
|
||||
@@ -16,13 +17,17 @@ type stubNixOS struct {
|
||||
usernameErr map[string]error
|
||||
}
|
||||
|
||||
func (s *stubNixOS) Geteuid() int { return 1971 }
|
||||
func (s *stubNixOS) Getuid() int { return 1971 }
|
||||
func (s *stubNixOS) Getgid() int { return 100 }
|
||||
func (s *stubNixOS) TempDir() string { return "/tmp" }
|
||||
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/fortify" }
|
||||
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/hakurei" }
|
||||
func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) }
|
||||
func (s *stubNixOS) EvalSymlinks(path string) (string, error) { return path, nil }
|
||||
func (s *stubNixOS) Uid(aid int) (int, error) { return 1000000 + 0*10000 + aid, nil }
|
||||
|
||||
func (s *stubNixOS) Println(v ...any) { log.Println(v...) }
|
||||
func (s *stubNixOS) Printf(format string, v ...any) { log.Printf(format, v...) }
|
||||
|
||||
func (s *stubNixOS) LookupEnv(key string) (string, bool) {
|
||||
switch key {
|
||||
case "SHELL":
|
||||
@@ -50,10 +55,8 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
|
||||
}
|
||||
|
||||
switch file {
|
||||
case "sudo":
|
||||
return "/run/wrappers/bin/sudo", nil
|
||||
case "machinectl":
|
||||
return "/home/ophestra/.nix-profile/bin/machinectl", nil
|
||||
case "zsh":
|
||||
return "/run/current-system/sw/bin/zsh", nil
|
||||
default:
|
||||
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
|
||||
}
|
||||
@@ -122,10 +125,10 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stubNixOS) Paths() linux.Paths {
|
||||
return linux.Paths{
|
||||
SharePath: "/tmp/fortify.1971",
|
||||
func (s *stubNixOS) Paths() hst.Paths {
|
||||
return hst.Paths{
|
||||
SharePath: "/tmp/hakurei.1971",
|
||||
RuntimePath: "/run/user/1971",
|
||||
RunDirPath: "/run/user/1971/fortify",
|
||||
RunDirPath: "/run/user/1971/hakurei",
|
||||
}
|
||||
}
|
||||
104
cmd/hakurei/internal/app/internal/setuid/app_test.go
Normal file
104
cmd/hakurei/internal/app/internal/setuid/app_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package setuid_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
type sealTestCase struct {
|
||||
name string
|
||||
os sys.State
|
||||
config *hst.Config
|
||||
id app.ID
|
||||
wantSys *system.I
|
||||
wantContainer *container.Params
|
||||
}
|
||||
|
||||
func TestApp(t *testing.T) {
|
||||
testCases := append(testCasesPd, testCasesNixos...)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := setuid.NewWithID(tc.id, tc.os)
|
||||
var (
|
||||
gotSys *system.I
|
||||
gotContainer *container.Params
|
||||
)
|
||||
if !t.Run("seal", func(t *testing.T) {
|
||||
if sa, err := a.Seal(tc.config); err != nil {
|
||||
t.Errorf("Seal: error = %v", err)
|
||||
return
|
||||
} else {
|
||||
gotSys, gotContainer = setuid.AppIParams(a, sa)
|
||||
}
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("compare sys", func(t *testing.T) {
|
||||
if !gotSys.Equal(tc.wantSys) {
|
||||
t.Errorf("Seal: sys = %#v, want %#v",
|
||||
gotSys, tc.wantSys)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("compare params", func(t *testing.T) {
|
||||
if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
|
||||
t.Errorf("seal: params =\n%s\n, want\n%s",
|
||||
mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshal(v any) string {
|
||||
if b, err := json.Marshal(v); err != nil {
|
||||
panic(err.Error())
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
|
||||
e = make([]fs.DirEntry, len(names))
|
||||
for i, name := range names {
|
||||
e[i] = stubDirEntryPath(name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type stubDirEntryPath string
|
||||
|
||||
func (p stubDirEntryPath) Name() string { return string(p) }
|
||||
func (p stubDirEntryPath) IsDir() bool { panic("attempted to call IsDir") }
|
||||
func (p stubDirEntryPath) Type() fs.FileMode { panic("attempted to call Type") }
|
||||
func (p stubDirEntryPath) Info() (fs.FileInfo, error) { panic("attempted to call Info") }
|
||||
|
||||
type stubFileInfoMode fs.FileMode
|
||||
|
||||
func (s stubFileInfoMode) Name() string { panic("attempted to call Name") }
|
||||
func (s stubFileInfoMode) Size() int64 { panic("attempted to call Size") }
|
||||
func (s stubFileInfoMode) Mode() fs.FileMode { return fs.FileMode(s) }
|
||||
func (s stubFileInfoMode) ModTime() time.Time { panic("attempted to call ModTime") }
|
||||
func (s stubFileInfoMode) IsDir() bool { panic("attempted to call IsDir") }
|
||||
func (s stubFileInfoMode) Sys() any { panic("attempted to call Sys") }
|
||||
|
||||
type stubFileInfoIsDir bool
|
||||
|
||||
func (s stubFileInfoIsDir) Name() string { panic("attempted to call Name") }
|
||||
func (s stubFileInfoIsDir) Size() int64 { panic("attempted to call Size") }
|
||||
func (s stubFileInfoIsDir) Mode() fs.FileMode { panic("attempted to call Mode") }
|
||||
func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTime") }
|
||||
func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
|
||||
func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }
|
||||
182
cmd/hakurei/internal/app/internal/setuid/errors.go
Normal file
182
cmd/hakurei/internal/app/internal/setuid/errors.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package setuid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||
code = rs.ExitStatus()
|
||||
|
||||
if runErr != nil {
|
||||
if rs.Time == nil {
|
||||
hlog.PrintBaseError(runErr, "cannot start app:")
|
||||
} else {
|
||||
var e *hlog.BaseError
|
||||
if !hlog.AsBaseError(runErr, &e) {
|
||||
log.Println("wait failed:", runErr)
|
||||
} else {
|
||||
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
|
||||
var se *StateStoreError
|
||||
if !errors.As(runErr, &se) {
|
||||
// does not need special handling
|
||||
log.Print(e.Message())
|
||||
} else {
|
||||
// inner error are either unwrapped store errors
|
||||
// or joined errors returned by *appSealTx revert
|
||||
// wrapped in *app.BaseError
|
||||
var ej RevertCompoundError
|
||||
if !errors.As(se.InnerErr, &ej) {
|
||||
// does not require special handling
|
||||
log.Print(e.Message())
|
||||
} else {
|
||||
errs := ej.Unwrap()
|
||||
|
||||
// every error here is wrapped in *app.BaseError
|
||||
for _, ei := range errs {
|
||||
var eb *hlog.BaseError
|
||||
if !errors.As(ei, &eb) {
|
||||
// unreachable
|
||||
log.Println("invalid error type returned by revert:", ei)
|
||||
} else {
|
||||
// print inner *app.BaseError message
|
||||
log.Print(eb.Message())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if code == 0 {
|
||||
code = 126
|
||||
}
|
||||
}
|
||||
|
||||
if rs.RevertErr != nil {
|
||||
var stateStoreError *StateStoreError
|
||||
if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil {
|
||||
hlog.PrintBaseError(rs.RevertErr, "generic fault during cleanup:")
|
||||
goto out
|
||||
}
|
||||
|
||||
if stateStoreError.Err != nil {
|
||||
if len(stateStoreError.Err) == 2 {
|
||||
if stateStoreError.Err[0] != nil {
|
||||
if joinedErrs, ok := stateStoreError.Err[0].(interface{ Unwrap() []error }); !ok {
|
||||
hlog.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:")
|
||||
} else {
|
||||
for _, err := range joinedErrs.Unwrap() {
|
||||
if err != nil {
|
||||
hlog.PrintBaseError(err, "fault during revert:")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if stateStoreError.Err[1] != nil {
|
||||
log.Printf("cannot close store: %v", stateStoreError.Err[1])
|
||||
}
|
||||
} else {
|
||||
log.Printf("fault during cleanup: %v",
|
||||
errors.Join(stateStoreError.Err...))
|
||||
}
|
||||
}
|
||||
|
||||
if stateStoreError.OpErr != nil {
|
||||
log.Printf("blind revert due to store fault: %v",
|
||||
stateStoreError.OpErr)
|
||||
}
|
||||
|
||||
if stateStoreError.DoErr != nil {
|
||||
hlog.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:")
|
||||
}
|
||||
|
||||
if stateStoreError.Inner && stateStoreError.InnerErr != nil {
|
||||
hlog.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:")
|
||||
}
|
||||
|
||||
out:
|
||||
if code == 0 {
|
||||
code = 128
|
||||
}
|
||||
}
|
||||
if rs.WaitErr != nil {
|
||||
hlog.Verbosef("wait: %v", rs.WaitErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StateStoreError is returned for a failed state save
|
||||
type StateStoreError struct {
|
||||
// whether inner function was called
|
||||
Inner bool
|
||||
// returned by the Save/Destroy method of [state.Cursor]
|
||||
InnerErr error
|
||||
// returned by the Do method of [state.Store]
|
||||
DoErr error
|
||||
// stores an arbitrary store operation error
|
||||
OpErr error
|
||||
// stores arbitrary errors
|
||||
Err []error
|
||||
}
|
||||
|
||||
// save saves arbitrary errors in [StateStoreError] once.
|
||||
func (e *StateStoreError) save(errs ...error) {
|
||||
if len(errs) == 0 || e.Err != nil {
|
||||
panic("invalid call to save")
|
||||
}
|
||||
e.Err = errs
|
||||
}
|
||||
|
||||
func (e *StateStoreError) equiv(a ...any) error {
|
||||
if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Err...) == nil {
|
||||
return nil
|
||||
} else {
|
||||
return hlog.WrapErrSuffix(e, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *StateStoreError) Error() string {
|
||||
if e.Inner && e.InnerErr != nil {
|
||||
return e.InnerErr.Error()
|
||||
}
|
||||
if e.DoErr != nil {
|
||||
return e.DoErr.Error()
|
||||
}
|
||||
if e.OpErr != nil {
|
||||
return e.OpErr.Error()
|
||||
}
|
||||
if err := errors.Join(e.Err...); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// equiv nullifies e for values where this is reached
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (e *StateStoreError) Unwrap() (errs []error) {
|
||||
errs = make([]error, 0, 3)
|
||||
if e.InnerErr != nil {
|
||||
errs = append(errs, e.InnerErr)
|
||||
}
|
||||
if e.DoErr != nil {
|
||||
errs = append(errs, e.DoErr)
|
||||
}
|
||||
if e.OpErr != nil {
|
||||
errs = append(errs, e.OpErr)
|
||||
}
|
||||
if err := errors.Join(e.Err...); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// A RevertCompoundError encapsulates errors returned by
|
||||
// the Revert method of [system.I].
|
||||
type RevertCompoundError interface {
|
||||
Error() string
|
||||
Unwrap() []error
|
||||
}
|
||||
24
cmd/hakurei/internal/app/internal/setuid/export_test.go
Normal file
24
cmd/hakurei/internal/app/internal/setuid/export_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package setuid
|
||||
|
||||
import (
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
func NewWithID(id ID, os sys.State) App {
|
||||
a := new(app)
|
||||
a.id = newID(&id)
|
||||
a.sys = os
|
||||
return a
|
||||
}
|
||||
|
||||
func AppIParams(a App, sa SealedApp) (*system.I, *container.Params) {
|
||||
v := a.(*app)
|
||||
seal := sa.(*outcome)
|
||||
if v.outcome != seal || v.id != seal.id {
|
||||
panic("broken app/outcome link")
|
||||
}
|
||||
return seal.sys, seal.container
|
||||
}
|
||||
195
cmd/hakurei/internal/app/internal/setuid/process.go
Normal file
195
cmd/hakurei/internal/app/internal/setuid/process.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package setuid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
const shimWaitTimeout = 5 * time.Second
|
||||
|
||||
func (seal *outcome) Run(rs *RunState) error {
|
||||
if !seal.f.CompareAndSwap(false, true) {
|
||||
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
|
||||
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
|
||||
// other Run a chance to return
|
||||
return errors.New("outcome: attempted to run twice")
|
||||
}
|
||||
|
||||
if rs == nil {
|
||||
panic("invalid state")
|
||||
}
|
||||
|
||||
// read comp value early to allow for early failure
|
||||
hsuPath := internal.MustHsuPath()
|
||||
|
||||
if err := seal.sys.Commit(seal.ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
store := state.NewMulti(seal.runDirPath)
|
||||
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
|
||||
defer func() {
|
||||
var revertErr error
|
||||
storeErr := new(StateStoreError)
|
||||
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
|
||||
revertErr = func() error {
|
||||
storeErr.InnerErr = deferredStoreFunc(c)
|
||||
|
||||
var rt system.Enablement
|
||||
ec := system.Process
|
||||
if states, err := c.Load(); err != nil {
|
||||
// revert per-process state here to limit damage
|
||||
storeErr.OpErr = err
|
||||
return seal.sys.Revert((*system.Criteria)(&ec))
|
||||
} else {
|
||||
if l := len(states); l == 0 {
|
||||
ec |= system.User
|
||||
} else {
|
||||
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
|
||||
}
|
||||
|
||||
// accumulate enablements of remaining launchers
|
||||
for i, s := range states {
|
||||
if s.Config != nil {
|
||||
rt |= s.Config.Enablements
|
||||
} else {
|
||||
log.Printf("state entry %d does not contain config", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
|
||||
if hlog.Load() {
|
||||
if ec > 0 {
|
||||
hlog.Verbose("reverting operations scope", system.TypeString(ec))
|
||||
}
|
||||
}
|
||||
|
||||
return seal.sys.Revert((*system.Criteria)(&ec))
|
||||
}()
|
||||
})
|
||||
storeErr.save(revertErr, store.Close())
|
||||
rs.RevertErr = storeErr.equiv("error during cleanup:")
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(seal.ctx)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, hsuPath)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.Dir = "/" // container init enters final working directory
|
||||
// shim runs in the same session as monitor; see shim.go for behaviour
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
|
||||
|
||||
var e *gob.Encoder
|
||||
if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot create shim setup pipe:")
|
||||
} else {
|
||||
e = encoder
|
||||
cmd.Env = []string{
|
||||
// passed through to shim by hsu
|
||||
shimEnv + "=" + strconv.Itoa(fd),
|
||||
// interpreted by hsu
|
||||
"HAKUREI_APP_ID=" + seal.user.aid.String(),
|
||||
}
|
||||
}
|
||||
|
||||
if len(seal.user.supp) > 0 {
|
||||
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
|
||||
// interpreted by hsu
|
||||
cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
|
||||
}
|
||||
|
||||
hlog.Verbosef("setuid helper at %s", hsuPath)
|
||||
hlog.Suspend()
|
||||
if err := cmd.Start(); err != nil {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot start setuid wrapper:")
|
||||
}
|
||||
rs.SetStart()
|
||||
|
||||
// this prevents blocking forever on an early failure
|
||||
waitErr, setupErr := make(chan error, 1), make(chan error, 1)
|
||||
go func() { waitErr <- cmd.Wait(); cancel() }()
|
||||
go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, hlog.Load()}) }()
|
||||
|
||||
select {
|
||||
case err := <-setupErr:
|
||||
if err != nil {
|
||||
hlog.Resume()
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot transmit shim config:")
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
hlog.Resume()
|
||||
return hlog.WrapErr(syscall.ECANCELED,
|
||||
"shim setup canceled")
|
||||
}
|
||||
|
||||
// returned after blocking on waitErr
|
||||
var earlyStoreErr = new(StateStoreError)
|
||||
{
|
||||
// shim accepted setup payload, create process state
|
||||
sd := state.State{
|
||||
ID: seal.id.unwrap(),
|
||||
PID: cmd.Process.Pid,
|
||||
Time: *rs.Time,
|
||||
}
|
||||
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
|
||||
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
|
||||
})
|
||||
}
|
||||
|
||||
// state in store at this point, destroy defunct state entry on return
|
||||
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
|
||||
|
||||
waitTimeout := make(chan struct{})
|
||||
go func() { <-seal.ctx.Done(); time.Sleep(shimWaitTimeout); close(waitTimeout) }()
|
||||
|
||||
select {
|
||||
case rs.WaitErr = <-waitErr:
|
||||
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
|
||||
if hlog.Load() {
|
||||
switch {
|
||||
case rs.Exited():
|
||||
hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
|
||||
case rs.CoreDump():
|
||||
hlog.Verbosef("process %d dumped core", cmd.Process.Pid)
|
||||
case rs.Signaled():
|
||||
hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
|
||||
default:
|
||||
hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
|
||||
}
|
||||
}
|
||||
case <-waitTimeout:
|
||||
rs.WaitErr = syscall.ETIMEDOUT
|
||||
hlog.Resume()
|
||||
log.Printf("process %d did not terminate", cmd.Process.Pid)
|
||||
}
|
||||
|
||||
hlog.Resume()
|
||||
if seal.sync != nil {
|
||||
if err := seal.sync.Close(); err != nil {
|
||||
log.Printf("cannot close wayland security context: %v", err)
|
||||
}
|
||||
}
|
||||
if seal.dbusMsg != nil {
|
||||
seal.dbusMsg()
|
||||
}
|
||||
|
||||
return earlyStoreErr.equiv("cannot save process state:")
|
||||
}
|
||||
586
cmd/hakurei/internal/app/internal/setuid/seal.go
Normal file
586
cmd/hakurei/internal/app/internal/setuid/seal.go
Normal file
@@ -0,0 +1,586 @@
|
||||
package setuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/instance/common"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/system/wayland"
|
||||
)
|
||||
|
||||
const (
|
||||
home = "HOME"
|
||||
shell = "SHELL"
|
||||
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
xdgSessionClass = "XDG_SESSION_CLASS"
|
||||
xdgSessionType = "XDG_SESSION_TYPE"
|
||||
|
||||
term = "TERM"
|
||||
display = "DISPLAY"
|
||||
|
||||
pulseServer = "PULSE_SERVER"
|
||||
pulseCookie = "PULSE_COOKIE"
|
||||
|
||||
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
||||
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrConfig = errors.New("no configuration to seal")
|
||||
ErrUser = errors.New("invalid aid")
|
||||
ErrHome = errors.New("invalid home directory")
|
||||
ErrName = errors.New("invalid username")
|
||||
|
||||
ErrXDisplay = errors.New(display + " unset")
|
||||
|
||||
ErrPulseCookie = errors.New("pulse cookie not present")
|
||||
ErrPulseSocket = errors.New("pulse socket not present")
|
||||
ErrPulseMode = errors.New("unexpected pulse socket mode")
|
||||
)
|
||||
|
||||
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
|
||||
|
||||
// outcome stores copies of various parts of [hst.Config]
|
||||
type outcome struct {
|
||||
// copied from initialising [app]
|
||||
id *stringPair[ID]
|
||||
// copied from [sys.State] response
|
||||
runDirPath string
|
||||
|
||||
// initial [hst.Config] gob stream for state data;
|
||||
// this is prepared ahead of time as config is clobbered during seal creation
|
||||
ct io.WriterTo
|
||||
// dump dbus proxy message buffer
|
||||
dbusMsg func()
|
||||
|
||||
user hsuUser
|
||||
sys *system.I
|
||||
ctx context.Context
|
||||
|
||||
container *container.Params
|
||||
env map[string]string
|
||||
sync *os.File
|
||||
|
||||
f atomic.Bool
|
||||
}
|
||||
|
||||
// shareHost holds optional share directory state that must not be accessed directly
|
||||
type shareHost struct {
|
||||
// whether XDG_RUNTIME_DIR is used post hsu
|
||||
useRuntimeDir bool
|
||||
// process-specific directory in tmpdir, empty if unused
|
||||
sharePath string
|
||||
// process-specific directory in XDG_RUNTIME_DIR, empty if unused
|
||||
runtimeSharePath string
|
||||
|
||||
seal *outcome
|
||||
sc hst.Paths
|
||||
}
|
||||
|
||||
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
|
||||
func (share *shareHost) ensureRuntimeDir() {
|
||||
if share.useRuntimeDir {
|
||||
return
|
||||
}
|
||||
share.useRuntimeDir = true
|
||||
share.seal.sys.Ensure(share.sc.RunDirPath, 0700)
|
||||
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute)
|
||||
share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
|
||||
share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath, acl.Execute)
|
||||
}
|
||||
|
||||
// instance returns a process-specific share path within tmpdir
|
||||
func (share *shareHost) instance() string {
|
||||
if share.sharePath != "" {
|
||||
return share.sharePath
|
||||
}
|
||||
share.sharePath = path.Join(share.sc.SharePath, share.seal.id.String())
|
||||
share.seal.sys.Ephemeral(system.Process, share.sharePath, 0711)
|
||||
return share.sharePath
|
||||
}
|
||||
|
||||
// runtime returns a process-specific share path within XDG_RUNTIME_DIR
|
||||
func (share *shareHost) runtime() string {
|
||||
if share.runtimeSharePath != "" {
|
||||
return share.runtimeSharePath
|
||||
}
|
||||
share.ensureRuntimeDir()
|
||||
share.runtimeSharePath = path.Join(share.sc.RunDirPath, share.seal.id.String())
|
||||
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700)
|
||||
share.seal.sys.UpdatePerm(share.runtimeSharePath, acl.Execute)
|
||||
return share.runtimeSharePath
|
||||
}
|
||||
|
||||
// hsuUser stores post-hsu credentials and metadata
|
||||
type hsuUser struct {
|
||||
// application id
|
||||
aid *stringPair[int]
|
||||
// target uid resolved by fid:aid
|
||||
uid *stringPair[int]
|
||||
|
||||
// supplementary group ids
|
||||
supp []string
|
||||
|
||||
// home directory host path
|
||||
data string
|
||||
// app user home directory
|
||||
home string
|
||||
// passwd database username
|
||||
username string
|
||||
}
|
||||
|
||||
func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error {
|
||||
if seal.ctx != nil {
|
||||
panic("finalise called twice")
|
||||
}
|
||||
seal.ctx = ctx
|
||||
|
||||
{
|
||||
// encode initial configuration for state tracking
|
||||
ct := new(bytes.Buffer)
|
||||
if err := gob.NewEncoder(ct).Encode(config); err != nil {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot encode initial config:")
|
||||
}
|
||||
seal.ct = ct
|
||||
}
|
||||
|
||||
// allowed aid range 0 to 9999, this is checked again in hsu
|
||||
if config.Identity < 0 || config.Identity > 9999 {
|
||||
return hlog.WrapErr(ErrUser,
|
||||
fmt.Sprintf("identity %d out of range", config.Identity))
|
||||
}
|
||||
|
||||
seal.user = hsuUser{
|
||||
aid: newInt(config.Identity),
|
||||
data: config.Data,
|
||||
home: config.Dir,
|
||||
username: config.Username,
|
||||
}
|
||||
if seal.user.username == "" {
|
||||
seal.user.username = "chronos"
|
||||
} else if !posixUsername.MatchString(seal.user.username) ||
|
||||
len(seal.user.username) >= internal.Sysconf(internal.SC_LOGIN_NAME_MAX) {
|
||||
return hlog.WrapErr(ErrName,
|
||||
fmt.Sprintf("invalid user name %q", seal.user.username))
|
||||
}
|
||||
if seal.user.data == "" || !path.IsAbs(seal.user.data) {
|
||||
return hlog.WrapErr(ErrHome,
|
||||
fmt.Sprintf("invalid home directory %q", seal.user.data))
|
||||
}
|
||||
if seal.user.home == "" {
|
||||
seal.user.home = seal.user.data
|
||||
}
|
||||
if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil {
|
||||
return err
|
||||
} else {
|
||||
seal.user.uid = newInt(u)
|
||||
}
|
||||
seal.user.supp = make([]string, len(config.Groups))
|
||||
for i, name := range config.Groups {
|
||||
if g, err := sys.LookupGroup(name); err != nil {
|
||||
return hlog.WrapErr(err,
|
||||
fmt.Sprintf("unknown group %q", name))
|
||||
} else {
|
||||
seal.user.supp[i] = g.Gid
|
||||
}
|
||||
}
|
||||
|
||||
// this also falls back to host path if encountering an invalid path
|
||||
if !path.IsAbs(config.Shell) {
|
||||
config.Shell = "/bin/sh"
|
||||
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
|
||||
config.Shell = s
|
||||
}
|
||||
}
|
||||
// do not use the value of shell before this point
|
||||
|
||||
// permissive defaults
|
||||
if config.Container == nil {
|
||||
hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
|
||||
|
||||
// hsu clears the environment so resolve paths early
|
||||
if !path.IsAbs(config.Path) {
|
||||
if len(config.Args) > 0 {
|
||||
if p, err := sys.LookPath(config.Args[0]); err != nil {
|
||||
return hlog.WrapErr(err, err.Error())
|
||||
} else {
|
||||
config.Path = p
|
||||
}
|
||||
} else {
|
||||
config.Path = config.Shell
|
||||
}
|
||||
}
|
||||
|
||||
conf := &hst.ContainerConfig{
|
||||
Userns: true,
|
||||
Net: true,
|
||||
Tty: true,
|
||||
AutoEtc: true,
|
||||
}
|
||||
// bind entries in /
|
||||
if d, err := sys.ReadDir("/"); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b := make([]*hst.FilesystemConfig, 0, len(d))
|
||||
for _, ent := range d {
|
||||
p := "/" + ent.Name()
|
||||
switch p {
|
||||
case "/proc":
|
||||
case "/dev":
|
||||
case "/tmp":
|
||||
case "/mnt":
|
||||
case "/etc":
|
||||
|
||||
default:
|
||||
b = append(b, &hst.FilesystemConfig{Src: p, Write: true, Must: true})
|
||||
}
|
||||
}
|
||||
conf.Filesystem = append(conf.Filesystem, b...)
|
||||
}
|
||||
|
||||
// hide nscd from sandbox if present
|
||||
nscd := "/var/run/nscd"
|
||||
if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
|
||||
conf.Cover = append(conf.Cover, nscd)
|
||||
}
|
||||
// bind GPU stuff
|
||||
if config.Enablements&(system.EX11|system.EWayland) != 0 {
|
||||
conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: "/dev/dri", Device: true})
|
||||
}
|
||||
// opportunistically bind kvm
|
||||
conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: "/dev/kvm", Device: true})
|
||||
|
||||
config.Container = conf
|
||||
}
|
||||
|
||||
var mapuid, mapgid *stringPair[int]
|
||||
{
|
||||
var uid, gid int
|
||||
var err error
|
||||
seal.container, seal.env, err = common.NewContainer(config.Container, sys, &uid, &gid)
|
||||
if err != nil {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot initialise container configuration:")
|
||||
}
|
||||
if !path.IsAbs(config.Path) {
|
||||
return hlog.WrapErr(syscall.EINVAL,
|
||||
"invalid program path")
|
||||
}
|
||||
if len(config.Args) == 0 {
|
||||
config.Args = []string{config.Path}
|
||||
}
|
||||
seal.container.Path = config.Path
|
||||
seal.container.Args = config.Args
|
||||
|
||||
mapuid = newInt(uid)
|
||||
mapgid = newInt(gid)
|
||||
if seal.env == nil {
|
||||
seal.env = make(map[string]string, 1<<6)
|
||||
}
|
||||
}
|
||||
|
||||
if !config.Container.AutoEtc {
|
||||
if config.Container.Etc != "" {
|
||||
seal.container.Bind(config.Container.Etc, "/etc", 0)
|
||||
}
|
||||
} else {
|
||||
etcPath := config.Container.Etc
|
||||
if etcPath == "" {
|
||||
etcPath = "/etc"
|
||||
}
|
||||
seal.container.Etc(etcPath, seal.id.String())
|
||||
}
|
||||
|
||||
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
|
||||
innerRuntimeDir := path.Join("/run/user", mapuid.String())
|
||||
seal.env[xdgRuntimeDir] = innerRuntimeDir
|
||||
seal.env[xdgSessionClass] = "user"
|
||||
seal.env[xdgSessionType] = "tty"
|
||||
|
||||
share := &shareHost{seal: seal, sc: sys.Paths()}
|
||||
seal.runDirPath = share.sc.RunDirPath
|
||||
seal.sys = system.New(seal.user.uid.unwrap())
|
||||
seal.sys.Ensure(share.sc.SharePath, 0711)
|
||||
|
||||
{
|
||||
runtimeDir := path.Join(share.sc.SharePath, "runtime")
|
||||
seal.sys.Ensure(runtimeDir, 0700)
|
||||
seal.sys.UpdatePermType(system.User, runtimeDir, acl.Execute)
|
||||
runtimeDirInst := path.Join(runtimeDir, seal.user.aid.String())
|
||||
seal.sys.Ensure(runtimeDirInst, 0700)
|
||||
seal.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute)
|
||||
seal.container.Tmpfs("/run/user", 1<<12, 0755)
|
||||
seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir := path.Join(share.sc.SharePath, "tmpdir")
|
||||
seal.sys.Ensure(tmpdir, 0700)
|
||||
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
|
||||
tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
|
||||
seal.sys.Ensure(tmpdirInst, 01700)
|
||||
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
|
||||
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
|
||||
seal.container.Bind(tmpdirInst, "/tmp", container.BindWritable)
|
||||
}
|
||||
|
||||
{
|
||||
homeDir := "/var/empty"
|
||||
if seal.user.home != "" {
|
||||
homeDir = seal.user.home
|
||||
}
|
||||
username := "chronos"
|
||||
if seal.user.username != "" {
|
||||
username = seal.user.username
|
||||
}
|
||||
seal.container.Bind(seal.user.data, homeDir, container.BindWritable)
|
||||
seal.container.Dir = homeDir
|
||||
seal.env["HOME"] = homeDir
|
||||
seal.env["USER"] = username
|
||||
seal.env[shell] = config.Shell
|
||||
|
||||
seal.container.Place("/etc/passwd",
|
||||
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+homeDir+":"+config.Shell+"\n"))
|
||||
seal.container.Place("/etc/group",
|
||||
[]byte("hakurei:x:"+mapgid.String()+":\n"))
|
||||
}
|
||||
|
||||
// pass TERM for proper terminal I/O in initial process
|
||||
if t, ok := sys.LookupEnv(term); ok {
|
||||
seal.env[term] = t
|
||||
}
|
||||
|
||||
if config.Enablements&system.EWayland != 0 {
|
||||
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
||||
var socketPath string
|
||||
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
|
||||
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
|
||||
socketPath = path.Join(share.sc.RuntimePath, wayland.FallbackName)
|
||||
} else if !path.IsAbs(name) {
|
||||
socketPath = path.Join(share.sc.RuntimePath, name)
|
||||
} else {
|
||||
socketPath = name
|
||||
}
|
||||
|
||||
innerPath := path.Join(innerRuntimeDir, wayland.FallbackName)
|
||||
seal.env[wayland.WaylandDisplay] = wayland.FallbackName
|
||||
|
||||
if !config.DirectWayland { // set up security-context-v1
|
||||
appID := config.ID
|
||||
if appID == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appID = "app.hakurei." + seal.id.String()
|
||||
}
|
||||
// downstream socket paths
|
||||
outerPath := path.Join(share.instance(), "wayland")
|
||||
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
|
||||
seal.container.Bind(outerPath, innerPath, 0)
|
||||
} else { // bind mount wayland socket (insecure)
|
||||
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
||||
share.ensureRuntimeDir()
|
||||
seal.container.Bind(socketPath, innerPath, 0)
|
||||
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Enablements&system.EX11 != 0 {
|
||||
if d, ok := sys.LookupEnv(display); !ok {
|
||||
return hlog.WrapErr(ErrXDisplay,
|
||||
"DISPLAY is not set")
|
||||
} else {
|
||||
seal.sys.ChangeHosts("#" + seal.user.uid.String())
|
||||
seal.env[display] = d
|
||||
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Enablements&system.EPulse != 0 {
|
||||
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
|
||||
pulseRuntimeDir := path.Join(share.sc.RuntimePath, "pulse")
|
||||
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
|
||||
pulseSocket := path.Join(pulseRuntimeDir, "native")
|
||||
|
||||
if _, err := sys.Stat(pulseRuntimeDir); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
|
||||
}
|
||||
return hlog.WrapErr(ErrPulseSocket,
|
||||
fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
|
||||
}
|
||||
|
||||
if s, err := sys.Stat(pulseSocket); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
|
||||
}
|
||||
return hlog.WrapErr(ErrPulseSocket,
|
||||
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
return hlog.WrapErr(ErrPulseMode,
|
||||
fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m)
|
||||
}
|
||||
}
|
||||
|
||||
// hard link pulse socket into target-executable share
|
||||
innerPulseRuntimeDir := path.Join(share.runtime(), "pulse")
|
||||
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
|
||||
seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
|
||||
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
|
||||
seal.env[pulseServer] = "unix:" + innerPulseSocket
|
||||
|
||||
// publish current user's pulse cookie for target user
|
||||
if src, err := discoverPulseCookie(sys); err != nil {
|
||||
// not fatal
|
||||
hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message()))
|
||||
} else {
|
||||
innerDst := hst.Tmp + "/pulse-cookie"
|
||||
seal.env[pulseCookie] = innerDst
|
||||
var payload *[]byte
|
||||
seal.container.PlaceP(innerDst, &payload)
|
||||
seal.sys.CopyFile(payload, src, 256, 256)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Enablements&system.EDBus != 0 {
|
||||
// ensure dbus session bus defaults
|
||||
if config.SessionBus == nil {
|
||||
config.SessionBus = dbus.NewConfig(config.ID, true, true)
|
||||
}
|
||||
|
||||
// downstream socket paths
|
||||
sharePath := share.instance()
|
||||
sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket")
|
||||
|
||||
// configure dbus proxy
|
||||
if f, err := seal.sys.ProxyDBus(
|
||||
config.SessionBus, config.SystemBus,
|
||||
sessionPath, systemPath,
|
||||
); err != nil {
|
||||
return err
|
||||
} else {
|
||||
seal.dbusMsg = f
|
||||
}
|
||||
|
||||
// share proxy sockets
|
||||
sessionInner := path.Join(innerRuntimeDir, "bus")
|
||||
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
|
||||
seal.container.Bind(sessionPath, sessionInner, 0)
|
||||
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
|
||||
if config.SystemBus != nil {
|
||||
systemInner := "/run/dbus/system_bus_socket"
|
||||
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
|
||||
seal.container.Bind(systemPath, systemInner, 0)
|
||||
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
|
||||
}
|
||||
}
|
||||
|
||||
for _, dest := range config.Container.Cover {
|
||||
seal.container.Tmpfs(dest, 1<<13, 0755)
|
||||
}
|
||||
|
||||
// append ExtraPerms last
|
||||
for _, p := range config.ExtraPerms {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Ensure {
|
||||
seal.sys.Ensure(p.Path, 0700)
|
||||
}
|
||||
|
||||
perms := make(acl.Perms, 0, 3)
|
||||
if p.Read {
|
||||
perms = append(perms, acl.Read)
|
||||
}
|
||||
if p.Write {
|
||||
perms = append(perms, acl.Write)
|
||||
}
|
||||
if p.Execute {
|
||||
perms = append(perms, acl.Execute)
|
||||
}
|
||||
seal.sys.UpdatePermType(system.User, p.Path, perms...)
|
||||
}
|
||||
|
||||
// flatten and sort env for deterministic behaviour
|
||||
seal.container.Env = make([]string, 0, len(seal.env))
|
||||
for k, v := range seal.env {
|
||||
if strings.IndexByte(k, '=') != -1 {
|
||||
return hlog.WrapErr(syscall.EINVAL,
|
||||
fmt.Sprintf("invalid environment variable %s", k))
|
||||
}
|
||||
seal.container.Env = append(seal.container.Env, k+"="+v)
|
||||
}
|
||||
slices.Sort(seal.container.Env)
|
||||
|
||||
if hlog.Load() {
|
||||
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
|
||||
seal.user.uid, seal.user.username, config.Groups, seal.container.Args, len(*seal.container.Ops))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
|
||||
func discoverPulseCookie(sys sys.State) (string, error) {
|
||||
if p, ok := sys.LookupEnv(pulseCookie); ok {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// dotfile $HOME/.pulse-cookie
|
||||
if p, ok := sys.LookupEnv(home); ok {
|
||||
p = path.Join(p, ".pulse-cookie")
|
||||
if s, err := sys.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return p, hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
|
||||
}
|
||||
// not found, try next method
|
||||
} else if !s.IsDir() {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// $XDG_CONFIG_HOME/pulse/cookie
|
||||
if p, ok := sys.LookupEnv(xdgConfigHome); ok {
|
||||
p = path.Join(p, "pulse", "cookie")
|
||||
if s, err := sys.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return p, hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
|
||||
}
|
||||
// not found, try next method
|
||||
} else if !s.IsDir() {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", hlog.WrapErr(ErrPulseCookie,
|
||||
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
||||
pulseCookie, xdgConfigHome, home))
|
||||
}
|
||||
184
cmd/hakurei/internal/app/internal/setuid/shim.go
Normal file
184
cmd/hakurei/internal/app/internal/setuid/shim.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package setuid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
|
||||
static pid_t hakurei_shim_param_ppid = -1;
|
||||
|
||||
// this cannot unblock hlog since Go code is not async-signal-safe
|
||||
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
|
||||
if (sig != SIGCONT || si == NULL) {
|
||||
// unreachable
|
||||
fprintf(stderr, "sigaction: sa_sigaction got invalid siginfo\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// monitor requests shim exit
|
||||
if (si->si_pid == hakurei_shim_param_ppid)
|
||||
exit(254);
|
||||
|
||||
fprintf(stderr, "sigaction: got SIGCONT from process %d\n", si->si_pid);
|
||||
|
||||
// shim orphaned before monitor delivers a signal
|
||||
if (getppid() != hakurei_shim_param_ppid)
|
||||
exit(3);
|
||||
}
|
||||
|
||||
void hakurei_shim_setup_cont_signal(pid_t ppid) {
|
||||
struct sigaction new_action = {0}, old_action = {0};
|
||||
if (sigaction(SIGCONT, NULL, &old_action) != 0)
|
||||
return;
|
||||
if (old_action.sa_handler != SIG_DFL) {
|
||||
errno = ENOTRECOVERABLE;
|
||||
return;
|
||||
}
|
||||
|
||||
new_action.sa_sigaction = hakurei_shim_sigaction;
|
||||
if (sigemptyset(&new_action.sa_mask) != 0)
|
||||
return;
|
||||
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
|
||||
|
||||
if (sigaction(SIGCONT, &new_action, NULL) != 0)
|
||||
return;
|
||||
|
||||
errno = 0;
|
||||
hakurei_shim_param_ppid = ppid;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const shimEnv = "HAKUREI_SHIM"
|
||||
|
||||
type shimParams struct {
|
||||
// monitor pid, checked against ppid in signal handler
|
||||
Monitor int
|
||||
|
||||
// finalised container params
|
||||
Container *container.Params
|
||||
// path to outer home directory
|
||||
Home string
|
||||
|
||||
// verbosity pass through
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
|
||||
func ShimMain() {
|
||||
hlog.Prepare("shim")
|
||||
|
||||
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
}
|
||||
|
||||
var (
|
||||
params shimParams
|
||||
closeSetup func() error
|
||||
)
|
||||
if f, err := container.Receive(shimEnv, ¶ms, nil); err != nil {
|
||||
if errors.Is(err, container.ErrInvalid) {
|
||||
log.Fatal("invalid config descriptor")
|
||||
}
|
||||
if errors.Is(err, container.ErrNotSet) {
|
||||
log.Fatal("HAKUREI_SHIM not set")
|
||||
}
|
||||
|
||||
log.Fatalf("cannot receive shim setup params: %v", err)
|
||||
} else {
|
||||
internal.InstallOutput(params.Verbose)
|
||||
closeSetup = f
|
||||
|
||||
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
|
||||
if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
|
||||
log.Fatalf("cannot install SIGCONT handler: %v", err)
|
||||
}
|
||||
|
||||
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
|
||||
log.Fatalf("cannot set parent-death signal: %v", errno)
|
||||
}
|
||||
}
|
||||
|
||||
if params.Container == nil || params.Container.Ops == nil {
|
||||
log.Fatal("invalid container params")
|
||||
}
|
||||
|
||||
// close setup socket
|
||||
if err := closeSetup(); err != nil {
|
||||
log.Printf("cannot close setup pipe: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
// ensure home directory as target user
|
||||
if s, err := os.Stat(params.Home); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.Mkdir(params.Home, 0700); err != nil {
|
||||
log.Fatalf("cannot create home directory: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Fatalf("cannot access home directory: %v", err)
|
||||
}
|
||||
|
||||
// home directory is created, proceed
|
||||
} else if !s.IsDir() {
|
||||
log.Fatalf("path %q is not a directory", params.Home)
|
||||
}
|
||||
|
||||
var name string
|
||||
if len(params.Container.Args) > 0 {
|
||||
name = params.Container.Args[0]
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
z := container.New(ctx, name)
|
||||
z.Params = *params.Container
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
z.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
|
||||
z.WaitDelay = 2 * time.Second
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot start container:")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := z.Serve(); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot configure container:")
|
||||
}
|
||||
|
||||
if err := seccomp.Load(
|
||||
seccomp.Preset(seccomp.PresetStrict, seccomp.AllowMultiarch),
|
||||
seccomp.AllowMultiarch,
|
||||
); err != nil {
|
||||
log.Fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
|
||||
if err := z.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
os.Exit(2)
|
||||
}
|
||||
log.Printf("wait: %v", err)
|
||||
os.Exit(127)
|
||||
}
|
||||
os.Exit(exitError.ExitCode())
|
||||
}
|
||||
}
|
||||
19
cmd/hakurei/internal/app/internal/setuid/strings.go
Normal file
19
cmd/hakurei/internal/app/internal/setuid/strings.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package setuid
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
)
|
||||
|
||||
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
|
||||
func newID(id *ID) *stringPair[ID] { return &stringPair[ID]{*id, id.String()} }
|
||||
|
||||
// stringPair stores a value and its string representation.
|
||||
type stringPair[T comparable] struct {
|
||||
v T
|
||||
s string
|
||||
}
|
||||
|
||||
func (s *stringPair[T]) unwrap() T { return s.v }
|
||||
func (s *stringPair[T]) String() string { return s.s }
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
// fine-grained locking and access
|
||||
@@ -33,10 +34,10 @@ func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
|
||||
|
||||
// load or initialise new backend
|
||||
b := new(multiBackend)
|
||||
b.lock.Lock()
|
||||
if v, ok := s.backends.LoadOrStore(aid, b); ok {
|
||||
b = v.(*multiBackend)
|
||||
} else {
|
||||
b.lock.Lock()
|
||||
b.path = path.Join(s.base, strconv.Itoa(aid))
|
||||
|
||||
// ensure directory
|
||||
@@ -85,17 +86,17 @@ func (s *multiStore) List() ([]int, error) {
|
||||
for _, e := range entries {
|
||||
// skip non-directories
|
||||
if !e.IsDir() {
|
||||
fmsg.Verbosef("skipped non-directory entry %q", e.Name())
|
||||
hlog.Verbosef("skipped non-directory entry %q", e.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
// skip non-numerical names
|
||||
if v, err := strconv.Atoi(e.Name()); err != nil {
|
||||
fmsg.Verbosef("skipped non-aid entry %q", e.Name())
|
||||
hlog.Verbosef("skipped non-aid entry %q", e.Name())
|
||||
continue
|
||||
} else {
|
||||
if v < 0 || v > 9999 {
|
||||
fmsg.Verbosef("skipped out of bounds entry %q", e.Name())
|
||||
hlog.Verbosef("skipped out of bounds entry %q", e.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -129,7 +130,7 @@ type multiBackend struct {
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (b *multiBackend) filename(id *fst.ID) string {
|
||||
func (b *multiBackend) filename(id *app.ID) string {
|
||||
return path.Join(b.path, id.String())
|
||||
}
|
||||
|
||||
@@ -189,8 +190,8 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
|
||||
return nil, fmt.Errorf("unexpected directory %q in store", e.Name())
|
||||
}
|
||||
|
||||
id := new(fst.ID)
|
||||
if err := fst.ParseAppID(id, e.Name()); err != nil {
|
||||
id := new(app.ID)
|
||||
if err := app.ParseAppID(id, e.Name()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -231,7 +232,7 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
|
||||
}
|
||||
|
||||
// state file consists of an eight byte header, followed by concatenated gobs
|
||||
// of [fst.Config] and [State], if [State.Config] is not nil or offset < 0,
|
||||
// of [hst.Config] and [State], if [State.Config] is not nil or offset < 0,
|
||||
// the first gob is skipped
|
||||
func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error {
|
||||
offset := make([]byte, 8)
|
||||
@@ -268,7 +269,7 @@ func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error {
|
||||
return ErrNoConfig
|
||||
}
|
||||
|
||||
state.Config = new(fst.Config)
|
||||
state.Config = new(hst.Config)
|
||||
if _, err := r.Seek(8, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -335,7 +336,7 @@ func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *multiBackend) Destroy(id fst.ID) error {
|
||||
func (b *multiBackend) Destroy(id app.ID) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
9
cmd/hakurei/internal/state/multi_test.go
Normal file
9
cmd/hakurei/internal/state/multi_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package state_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
)
|
||||
|
||||
func TestMulti(t *testing.T) { testStore(t, state.NewMulti(t.TempDir())) }
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
var ErrNoConfig = errors.New("state does not contain config")
|
||||
|
||||
type Entries map[fst.ID]*State
|
||||
type Entries map[app.ID]*State
|
||||
|
||||
type Store interface {
|
||||
// Do calls f exactly once and ensures store exclusivity until f returns.
|
||||
@@ -29,19 +30,19 @@ type Store interface {
|
||||
// Cursor provides access to the store
|
||||
type Cursor interface {
|
||||
Save(state *State, configWriter io.WriterTo) error
|
||||
Destroy(id fst.ID) error
|
||||
Destroy(id app.ID) error
|
||||
Load() (Entries, error)
|
||||
Len() (int, error)
|
||||
}
|
||||
|
||||
// State is a fortify process's state
|
||||
// State is an instance state
|
||||
type State struct {
|
||||
// fortify instance id
|
||||
ID fst.ID `json:"instance"`
|
||||
// hakurei instance id
|
||||
ID app.ID `json:"instance"`
|
||||
// child process PID value
|
||||
PID int `json:"pid"`
|
||||
// sealed app configuration
|
||||
Config *fst.Config `json:"config"`
|
||||
Config *hst.Config `json:"config"`
|
||||
|
||||
// process start time
|
||||
Time time.Time `json:"time"`
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
func testStore(t *testing.T, s state.Store) {
|
||||
@@ -62,7 +63,7 @@ func testStore(t *testing.T, s state.Store) {
|
||||
&tc[i].state.ID)
|
||||
} else {
|
||||
got.Time = tc[i].state.Time
|
||||
tc[i].state.Config = fst.Template()
|
||||
tc[i].state.Config = hst.Template()
|
||||
if !reflect.DeepEqual(got, &tc[i].state) {
|
||||
t.Fatalf("Load: entry %s got %#v, want %#v",
|
||||
&tc[i].state.ID, got, &tc[i].state)
|
||||
@@ -96,7 +97,7 @@ func testStore(t *testing.T, s state.Store) {
|
||||
} else {
|
||||
slices.Sort(aids)
|
||||
want := []int{0, 1}
|
||||
if slices.Compare(aids, want) != 0 {
|
||||
if !slices.Equal(aids, want) {
|
||||
t.Fatalf("List() = %#v, want %#v", aids, want)
|
||||
}
|
||||
}
|
||||
@@ -133,10 +134,10 @@ func testStore(t *testing.T, s state.Store) {
|
||||
}
|
||||
|
||||
func makeState(t *testing.T, s *state.State, ct io.Writer) {
|
||||
if err := fst.NewAppID(&s.ID); err != nil {
|
||||
if err := app.NewAppID(&s.ID); err != nil {
|
||||
t.Fatalf("cannot create dummy state: %v", err)
|
||||
}
|
||||
if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil {
|
||||
if err := gob.NewEncoder(ct).Encode(hst.Template()); err != nil {
|
||||
t.Fatalf("cannot encode dummy config: %v", err)
|
||||
}
|
||||
s.PID = rand.Int()
|
||||
51
cmd/hakurei/main.go
Normal file
51
cmd/hakurei/main.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
// this works around go:embed '..' limitation
|
||||
//go:generate cp ../../LICENSE .
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
|
||||
//go:embed LICENSE
|
||||
license string
|
||||
)
|
||||
|
||||
func init() { hlog.Prepare("hakurei") }
|
||||
|
||||
var std sys.State = new(sys.Std)
|
||||
|
||||
func main() {
|
||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||
|
||||
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
// this catches faulty command handlers that fail to return before this point
|
||||
})
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
@@ -10,19 +10,19 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
func tryPath(name string) (config *fst.Config) {
|
||||
func tryPath(name string) (config *hst.Config) {
|
||||
var r io.Reader
|
||||
config = new(fst.Config)
|
||||
config = new(hst.Config)
|
||||
|
||||
if name != "-" {
|
||||
r = tryFd(name)
|
||||
if r == nil {
|
||||
fmsg.Verbose("load configuration from file")
|
||||
hlog.Verbose("load configuration from file")
|
||||
|
||||
if f, err := os.Open(name); err != nil {
|
||||
log.Fatalf("cannot access configuration file %q: %s", name, err)
|
||||
@@ -50,9 +50,12 @@ func tryPath(name string) (config *fst.Config) {
|
||||
|
||||
func tryFd(name string) io.ReadCloser {
|
||||
if v, err := strconv.Atoi(name); err != nil {
|
||||
fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
if !errors.Is(err, strconv.ErrSyntax) {
|
||||
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
hlog.Verbosef("trying config stream from %d", v)
|
||||
fd := uintptr(v)
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
|
||||
if errors.Is(errno, syscall.EBADF) {
|
||||
@@ -64,7 +67,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
|
||||
if len(name) <= 32 {
|
||||
likePrefix = true
|
||||
@@ -82,9 +85,9 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
|
||||
|
||||
// try to match from state store
|
||||
if likePrefix && len(name) >= 8 {
|
||||
fmsg.Verbose("argument looks like prefix")
|
||||
hlog.Verbose("argument looks like prefix")
|
||||
|
||||
s := state.NewMulti(sys.Paths().RunDirPath)
|
||||
s := state.NewMulti(std.Paths().RunDirPath)
|
||||
if entries, err := state.Join(s); err != nil {
|
||||
log.Printf("cannot join store: %v", err)
|
||||
// drop to fetch from file
|
||||
@@ -93,12 +96,12 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
|
||||
v := id.String()
|
||||
if strings.HasPrefix(v, name) {
|
||||
// match, use config from this state entry
|
||||
instance = entries[id]
|
||||
config = instance.Config
|
||||
entry = entries[id]
|
||||
config = entry.Config
|
||||
break
|
||||
}
|
||||
|
||||
fmsg.Verbosef("instance %s skipped", v)
|
||||
hlog.Verbosef("instance %s skipped", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,29 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func printShowSystem(output io.Writer, short bool) {
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
info := new(fst.Info)
|
||||
info := new(hst.Info)
|
||||
|
||||
// get fid by querying uid of aid 0
|
||||
if uid, err := sys.Uid(0); err != nil {
|
||||
log.Fatalf("cannot obtain uid from fsu: %v", err)
|
||||
if uid, err := std.Uid(0); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
info.User = (uid / 10000) - 100
|
||||
}
|
||||
@@ -39,8 +42,8 @@ func printShowSystem(output io.Writer, short bool) {
|
||||
|
||||
func printShowInstance(
|
||||
output io.Writer, now time.Time,
|
||||
instance *state.State, config *fst.Config,
|
||||
short bool) {
|
||||
instance *state.State, config *hst.Config,
|
||||
short, flagJSON bool) {
|
||||
if flagJSON {
|
||||
if instance != nil {
|
||||
printJSON(output, short, instance)
|
||||
@@ -53,7 +56,7 @@ func printShowInstance(
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
if config.Confinement.Sandbox == nil {
|
||||
if config.Container == nil {
|
||||
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
|
||||
}
|
||||
|
||||
@@ -66,19 +69,21 @@ func printShowInstance(
|
||||
|
||||
t.Printf("App\n")
|
||||
if config.ID != "" {
|
||||
t.Printf(" ID:\t%d (%s)\n", config.Confinement.AppID, config.ID)
|
||||
t.Printf(" Identity:\t%d (%s)\n", config.Identity, config.ID)
|
||||
} else {
|
||||
t.Printf(" ID:\t%d\n", config.Confinement.AppID)
|
||||
t.Printf(" Identity:\t%d\n", config.Identity)
|
||||
}
|
||||
t.Printf(" Enablements:\t%s\n", config.Confinement.Enablements.String())
|
||||
if len(config.Confinement.Groups) > 0 {
|
||||
t.Printf(" Groups:\t%q\n", config.Confinement.Groups)
|
||||
t.Printf(" Enablements:\t%s\n", config.Enablements.String())
|
||||
if len(config.Groups) > 0 {
|
||||
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
||||
}
|
||||
t.Printf(" Directory:\t%s\n", config.Confinement.Outer)
|
||||
if config.Confinement.Sandbox != nil {
|
||||
sandbox := config.Confinement.Sandbox
|
||||
if sandbox.Hostname != "" {
|
||||
t.Printf(" Hostname:\t%q\n", sandbox.Hostname)
|
||||
if config.Data != "" {
|
||||
t.Printf(" Data:\t%s\n", config.Data)
|
||||
}
|
||||
if config.Container != nil {
|
||||
container := config.Container
|
||||
if container.Hostname != "" {
|
||||
t.Printf(" Hostname:\t%s\n", container.Hostname)
|
||||
}
|
||||
flags := make([]string, 0, 7)
|
||||
writeFlag := func(name string, value bool) {
|
||||
@@ -86,38 +91,40 @@ func printShowInstance(
|
||||
flags = append(flags, name)
|
||||
}
|
||||
}
|
||||
writeFlag("userns", sandbox.UserNS)
|
||||
writeFlag("net", sandbox.Net)
|
||||
writeFlag("dev", sandbox.Dev)
|
||||
writeFlag("tty", sandbox.NoNewSession)
|
||||
writeFlag("mapuid", sandbox.MapRealUID)
|
||||
writeFlag("directwl", sandbox.DirectWayland)
|
||||
writeFlag("autoetc", sandbox.AutoEtc)
|
||||
writeFlag("userns", container.Userns)
|
||||
writeFlag("devel", container.Devel)
|
||||
writeFlag("net", container.Net)
|
||||
writeFlag("device", container.Device)
|
||||
writeFlag("tty", container.Tty)
|
||||
writeFlag("mapuid", container.MapRealUID)
|
||||
writeFlag("directwl", config.DirectWayland)
|
||||
writeFlag("autoetc", container.AutoEtc)
|
||||
if len(flags) == 0 {
|
||||
flags = append(flags, "none")
|
||||
}
|
||||
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
|
||||
|
||||
etc := sandbox.Etc
|
||||
etc := container.Etc
|
||||
if etc == "" {
|
||||
etc = "/etc"
|
||||
}
|
||||
t.Printf(" Etc:\t%s\n", etc)
|
||||
|
||||
if len(sandbox.Override) > 0 {
|
||||
t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
|
||||
if len(container.Cover) > 0 {
|
||||
t.Printf(" Cover:\t%s\n", strings.Join(container.Cover, " "))
|
||||
}
|
||||
|
||||
// Env map[string]string `json:"env"`
|
||||
// Link [][2]string `json:"symlink"`
|
||||
t.Printf(" Path:\t%s\n", config.Path)
|
||||
}
|
||||
if len(config.Args) > 0 {
|
||||
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
|
||||
}
|
||||
t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
|
||||
t.Printf("\n")
|
||||
|
||||
if !short {
|
||||
if config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 {
|
||||
if config.Container != nil && len(config.Container.Filesystem) > 0 {
|
||||
t.Printf("Filesystem\n")
|
||||
for _, f := range config.Confinement.Sandbox.Filesystem {
|
||||
for _, f := range config.Container.Filesystem {
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
@@ -145,9 +152,9 @@ func printShowInstance(
|
||||
}
|
||||
t.Printf("\n")
|
||||
}
|
||||
if len(config.Confinement.ExtraPerms) > 0 {
|
||||
if len(config.ExtraPerms) > 0 {
|
||||
t.Printf("Extra ACL\n")
|
||||
for _, p := range config.Confinement.ExtraPerms {
|
||||
for _, p := range config.ExtraPerms {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
@@ -175,19 +182,19 @@ func printShowInstance(
|
||||
t.Printf(" Broadcast:\t%q\n", c.Broadcast)
|
||||
}
|
||||
}
|
||||
if config.Confinement.SessionBus != nil {
|
||||
if config.SessionBus != nil {
|
||||
t.Printf("Session bus\n")
|
||||
printDBus(config.Confinement.SessionBus)
|
||||
printDBus(config.SessionBus)
|
||||
t.Printf("\n")
|
||||
}
|
||||
if config.Confinement.SystemBus != nil {
|
||||
if config.SystemBus != nil {
|
||||
t.Printf("System bus\n")
|
||||
printDBus(config.Confinement.SystemBus)
|
||||
printDBus(config.SystemBus)
|
||||
t.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
|
||||
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
|
||||
var entries state.Entries
|
||||
if e, err := state.Join(s); err != nil {
|
||||
log.Fatalf("cannot join store: %v", err)
|
||||
@@ -244,22 +251,26 @@ func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
|
||||
t.Println("\tInstance\tPID\tApplication\tUptime")
|
||||
for _, e := range exp {
|
||||
var (
|
||||
es = "(No confinement information)"
|
||||
cs = "(No command information)"
|
||||
as = "(No configuration information)"
|
||||
)
|
||||
if e.Config != nil {
|
||||
es = e.Config.Confinement.Enablements.String()
|
||||
cs = fmt.Sprintf("%q", e.Config.Command)
|
||||
as = strconv.Itoa(e.Config.Confinement.AppID)
|
||||
if len(e.s) != 1<<5 {
|
||||
// unreachable
|
||||
log.Printf("possible store corruption: invalid instance string %s", e.s)
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
var (
|
||||
testID = fst.ID{
|
||||
testID = app.ID{
|
||||
0x8e, 0x2c, 0x76, 0xb0,
|
||||
0x66, 0xda, 0xbe, 0x57,
|
||||
0x4c, 0xf0, 0x73, 0xbd,
|
||||
@@ -20,7 +21,7 @@ var (
|
||||
testState = &state.State{
|
||||
ID: testID,
|
||||
PID: 0xDEADBEEF,
|
||||
Config: fst.Template(),
|
||||
Config: hst.Template(),
|
||||
Time: testAppTime,
|
||||
}
|
||||
testTime = time.Unix(3752, 1).UTC()
|
||||
@@ -31,32 +32,33 @@ func Test_printShowInstance(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
instance *state.State
|
||||
config *fst.Config
|
||||
config *hst.Config
|
||||
short, json bool
|
||||
want string
|
||||
}{
|
||||
{"config", nil, fst.Template(), false, false, `App
|
||||
ID: 9 (org.chromium.Chromium)
|
||||
Enablements: Wayland, D-Bus, PulseAudio
|
||||
Groups: ["video"]
|
||||
Directory: /var/lib/persist/home/org.chromium.Chromium
|
||||
Hostname: "localhost"
|
||||
Flags: userns net dev tty mapuid autoetc
|
||||
{"config", nil, hst.Template(), false, false, `App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Data: /var/lib/hakurei/u0/org.chromium.Chromium
|
||||
Hostname: localhost
|
||||
Flags: userns devel net device tty mapuid autoetc
|
||||
Etc: /etc
|
||||
Overrides: /var/run/nscd
|
||||
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||
Cover: /var/run/nscd
|
||||
Path: /run/current-system/sw/bin/chromium
|
||||
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||
|
||||
Filesystem
|
||||
+/nix/store
|
||||
+/run/current-system
|
||||
+/run/opengl-driver
|
||||
+/var/db/nix-channels
|
||||
w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
d+/dev/dri
|
||||
|
||||
Extra ACL
|
||||
--x+:/var/lib/fortify/u0
|
||||
rwx:/var/lib/fortify/u0/org.chromium.Chromium
|
||||
--x+:/var/lib/hakurei/u0
|
||||
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
|
||||
|
||||
Session bus
|
||||
Filter: true
|
||||
@@ -70,44 +72,38 @@ System bus
|
||||
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
|
||||
|
||||
`},
|
||||
{"config pd", nil, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
App
|
||||
ID: 0
|
||||
Enablements: (No enablements)
|
||||
Directory:
|
||||
Command:
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
`},
|
||||
{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
|
||||
ID: 0
|
||||
Enablements: (No enablements)
|
||||
Directory:
|
||||
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
Flags: none
|
||||
Etc: /etc
|
||||
Command:
|
||||
Path:
|
||||
|
||||
`},
|
||||
{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
|
||||
ID: 0
|
||||
Enablements: (No enablements)
|
||||
Directory:
|
||||
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]*hst.FilesystemConfig, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
Flags: none
|
||||
Etc: /etc
|
||||
Command:
|
||||
Path:
|
||||
|
||||
Filesystem
|
||||
|
||||
Extra ACL
|
||||
|
||||
`},
|
||||
{"config pd dbus see", nil, &fst.Config{Confinement: fst.ConfinementConfig{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}}, false, false, `Warning: this configuration uses permissive defaults!
|
||||
{"config pd dbus see", nil, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
App
|
||||
ID: 0
|
||||
Enablements: (No enablements)
|
||||
Directory:
|
||||
Command:
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
Session bus
|
||||
Filter: false
|
||||
@@ -115,32 +111,33 @@ Session bus
|
||||
|
||||
`},
|
||||
|
||||
{"instance", testState, fst.Template(), false, false, `State
|
||||
{"instance", testState, hst.Template(), false, false, `State
|
||||
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
|
||||
Uptime: 1h2m32s
|
||||
|
||||
App
|
||||
ID: 9 (org.chromium.Chromium)
|
||||
Enablements: Wayland, D-Bus, PulseAudio
|
||||
Groups: ["video"]
|
||||
Directory: /var/lib/persist/home/org.chromium.Chromium
|
||||
Hostname: "localhost"
|
||||
Flags: userns net dev tty mapuid autoetc
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Data: /var/lib/hakurei/u0/org.chromium.Chromium
|
||||
Hostname: localhost
|
||||
Flags: userns devel net device tty mapuid autoetc
|
||||
Etc: /etc
|
||||
Overrides: /var/run/nscd
|
||||
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||
Cover: /var/run/nscd
|
||||
Path: /run/current-system/sw/bin/chromium
|
||||
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||
|
||||
Filesystem
|
||||
+/nix/store
|
||||
+/run/current-system
|
||||
+/run/opengl-driver
|
||||
+/var/db/nix-channels
|
||||
w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
d+/dev/dri
|
||||
|
||||
Extra ACL
|
||||
--x+:/var/lib/fortify/u0
|
||||
rwx:/var/lib/fortify/u0/org.chromium.Chromium
|
||||
--x+:/var/lib/hakurei/u0
|
||||
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
|
||||
|
||||
Session bus
|
||||
Filter: true
|
||||
@@ -154,17 +151,15 @@ System bus
|
||||
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
|
||||
|
||||
`},
|
||||
{"instance pd", testState, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
State
|
||||
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
|
||||
Uptime: 1h2m32s
|
||||
|
||||
App
|
||||
ID: 0
|
||||
Enablements: (No enablements)
|
||||
Directory:
|
||||
Command:
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
`},
|
||||
|
||||
@@ -192,230 +187,15 @@ App
|
||||
"pid": 3735928559,
|
||||
"config": {
|
||||
"id": "org.chromium.Chromium",
|
||||
"command": [
|
||||
"path": "/run/current-system/sw/bin/chromium",
|
||||
"args": [
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland"
|
||||
],
|
||||
"confinement": {
|
||||
"app_id": 9,
|
||||
"groups": [
|
||||
"video"
|
||||
],
|
||||
"username": "chronos",
|
||||
"home_inner": "/var/lib/fortify",
|
||||
"home": "/var/lib/persist/home/org.chromium.Chromium",
|
||||
"sandbox": {
|
||||
"hostname": "localhost",
|
||||
"userns": true,
|
||||
"net": true,
|
||||
"dev": true,
|
||||
"syscall": {
|
||||
"compat": false,
|
||||
"deny_devel": true,
|
||||
"multiarch": true,
|
||||
"linux32": false,
|
||||
"can": false,
|
||||
"bluetooth": false
|
||||
},
|
||||
"no_new_session": true,
|
||||
"map_real_uid": true,
|
||||
"env": {
|
||||
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
|
||||
},
|
||||
"filesystem": [
|
||||
{
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"src": "/run/current-system"
|
||||
},
|
||||
{
|
||||
"src": "/run/opengl-driver"
|
||||
},
|
||||
{
|
||||
"src": "/var/db/nix-channels"
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
{
|
||||
"src": "/dev/dri",
|
||||
"dev": true
|
||||
}
|
||||
],
|
||||
"symlink": [
|
||||
[
|
||||
"/run/user/65534",
|
||||
"/run/user/150"
|
||||
]
|
||||
],
|
||||
"etc": "/etc",
|
||||
"auto_etc": true,
|
||||
"override": [
|
||||
"/var/run/nscd"
|
||||
]
|
||||
},
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/fortify/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
}
|
||||
],
|
||||
"system_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower"
|
||||
],
|
||||
"own": null,
|
||||
"call": null,
|
||||
"broadcast": null,
|
||||
"filter": 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
|
||||
},
|
||||
"enablements": 13
|
||||
}
|
||||
},
|
||||
"time": "1970-01-01T00:00:00.000000009Z"
|
||||
}
|
||||
`},
|
||||
{"json config", nil, fst.Template(), false, true, `{
|
||||
"id": "org.chromium.Chromium",
|
||||
"command": [
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland"
|
||||
],
|
||||
"confinement": {
|
||||
"app_id": 9,
|
||||
"groups": [
|
||||
"video"
|
||||
],
|
||||
"username": "chronos",
|
||||
"home_inner": "/var/lib/fortify",
|
||||
"home": "/var/lib/persist/home/org.chromium.Chromium",
|
||||
"sandbox": {
|
||||
"hostname": "localhost",
|
||||
"userns": true,
|
||||
"net": true,
|
||||
"dev": true,
|
||||
"syscall": {
|
||||
"compat": false,
|
||||
"deny_devel": true,
|
||||
"multiarch": true,
|
||||
"linux32": false,
|
||||
"can": false,
|
||||
"bluetooth": false
|
||||
},
|
||||
"no_new_session": true,
|
||||
"map_real_uid": true,
|
||||
"env": {
|
||||
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
|
||||
},
|
||||
"filesystem": [
|
||||
{
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"src": "/run/current-system"
|
||||
},
|
||||
{
|
||||
"src": "/run/opengl-driver"
|
||||
},
|
||||
{
|
||||
"src": "/var/db/nix-channels"
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
{
|
||||
"src": "/dev/dri",
|
||||
"dev": true
|
||||
}
|
||||
],
|
||||
"symlink": [
|
||||
[
|
||||
"/run/user/65534",
|
||||
"/run/user/150"
|
||||
]
|
||||
],
|
||||
"etc": "/etc",
|
||||
"auto_etc": true,
|
||||
"override": [
|
||||
"/var/run/nscd"
|
||||
]
|
||||
},
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/fortify/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
}
|
||||
],
|
||||
"system_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower"
|
||||
],
|
||||
"own": null,
|
||||
"call": null,
|
||||
"broadcast": null,
|
||||
"filter": true
|
||||
},
|
||||
"enablements": 13,
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
@@ -440,7 +220,218 @@ App
|
||||
},
|
||||
"filter": true
|
||||
},
|
||||
"enablements": 13
|
||||
"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",
|
||||
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"dir": "/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",
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"net": 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": [
|
||||
{
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"src": "/run/current-system"
|
||||
},
|
||||
{
|
||||
"src": "/run/opengl-driver"
|
||||
},
|
||||
{
|
||||
"src": "/var/db/nix-channels"
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
{
|
||||
"src": "/dev/dri",
|
||||
"dev": true
|
||||
}
|
||||
],
|
||||
"symlink": [
|
||||
[
|
||||
"/run/user/65534",
|
||||
"/run/user/150"
|
||||
]
|
||||
],
|
||||
"etc": "/etc",
|
||||
"auto_etc": true,
|
||||
"cover": [
|
||||
"/var/run/nscd"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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": 13,
|
||||
"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",
|
||||
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"dir": "/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",
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"net": 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": [
|
||||
{
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"src": "/run/current-system"
|
||||
},
|
||||
{
|
||||
"src": "/run/opengl-driver"
|
||||
},
|
||||
{
|
||||
"src": "/var/db/nix-channels"
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
{
|
||||
"src": "/dev/dri",
|
||||
"dev": true
|
||||
}
|
||||
],
|
||||
"symlink": [
|
||||
[
|
||||
"/run/user/65534",
|
||||
"/run/user/150"
|
||||
]
|
||||
],
|
||||
"etc": "/etc",
|
||||
"auto_etc": true,
|
||||
"cover": [
|
||||
"/var/run/nscd"
|
||||
]
|
||||
}
|
||||
}
|
||||
`},
|
||||
@@ -448,14 +439,8 @@ App
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
{
|
||||
v := flagJSON
|
||||
t.Cleanup(func() { flagJSON = v })
|
||||
flagJSON = tc.json
|
||||
}
|
||||
|
||||
output := new(strings.Builder)
|
||||
printShowInstance(output, testTime, tc.instance, tc.config, tc.short)
|
||||
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)
|
||||
@@ -472,23 +457,19 @@ func Test_printPs(t *testing.T) {
|
||||
short, json bool
|
||||
want string
|
||||
}{
|
||||
{"no entries", make(state.Entries), false, false, ` Instance PID App Uptime Enablements Command
|
||||
|
||||
`},
|
||||
{"no entries short", make(state.Entries), true, false, ``},
|
||||
{"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID App Uptime Enablements Command
|
||||
|
||||
`},
|
||||
{"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID App Uptime Enablements Command
|
||||
{"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{app.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 App Uptime Enablements Command
|
||||
8e2c76b0 3735928559 9 1h2m32s Wayland, D-Bus, PulseAudio ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
|
||||
|
||||
`},
|
||||
{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
|
||||
{"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": [
|
||||
@@ -512,127 +493,125 @@ func Test_printPs(t *testing.T) {
|
||||
"pid": 3735928559,
|
||||
"config": {
|
||||
"id": "org.chromium.Chromium",
|
||||
"command": [
|
||||
"path": "/run/current-system/sw/bin/chromium",
|
||||
"args": [
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland"
|
||||
],
|
||||
"confinement": {
|
||||
"app_id": 9,
|
||||
"groups": [
|
||||
"video"
|
||||
"enablements": 13,
|
||||
"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"
|
||||
],
|
||||
"username": "chronos",
|
||||
"home_inner": "/var/lib/fortify",
|
||||
"home": "/var/lib/persist/home/org.chromium.Chromium",
|
||||
"sandbox": {
|
||||
"hostname": "localhost",
|
||||
"userns": true,
|
||||
"net": true,
|
||||
"dev": true,
|
||||
"syscall": {
|
||||
"compat": false,
|
||||
"deny_devel": true,
|
||||
"multiarch": true,
|
||||
"linux32": false,
|
||||
"can": false,
|
||||
"bluetooth": false
|
||||
},
|
||||
"no_new_session": true,
|
||||
"map_real_uid": true,
|
||||
"env": {
|
||||
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
|
||||
},
|
||||
"filesystem": [
|
||||
{
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"src": "/run/current-system"
|
||||
},
|
||||
{
|
||||
"src": "/run/opengl-driver"
|
||||
},
|
||||
{
|
||||
"src": "/var/db/nix-channels"
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
{
|
||||
"src": "/dev/dri",
|
||||
"dev": true
|
||||
}
|
||||
],
|
||||
"symlink": [
|
||||
[
|
||||
"/run/user/65534",
|
||||
"/run/user/150"
|
||||
]
|
||||
],
|
||||
"etc": "/etc",
|
||||
"auto_etc": true,
|
||||
"override": [
|
||||
"/var/run/nscd"
|
||||
]
|
||||
"own": [
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*"
|
||||
],
|
||||
"call": {
|
||||
"org.freedesktop.portal.*": "*"
|
||||
},
|
||||
"extra_perms": [
|
||||
"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",
|
||||
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"dir": "/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",
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"net": 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": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/fortify/u0",
|
||||
"x": true
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
"src": "/run/current-system"
|
||||
},
|
||||
{
|
||||
"src": "/run/opengl-driver"
|
||||
},
|
||||
{
|
||||
"src": "/var/db/nix-channels"
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
{
|
||||
"src": "/dev/dri",
|
||||
"dev": true
|
||||
}
|
||||
],
|
||||
"system_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower"
|
||||
],
|
||||
"own": null,
|
||||
"call": null,
|
||||
"broadcast": null,
|
||||
"filter": 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
|
||||
},
|
||||
"enablements": 13
|
||||
"symlink": [
|
||||
[
|
||||
"/run/user/65534",
|
||||
"/run/user/150"
|
||||
]
|
||||
],
|
||||
"etc": "/etc",
|
||||
"auto_etc": true,
|
||||
"cover": [
|
||||
"/var/run/nscd"
|
||||
]
|
||||
}
|
||||
},
|
||||
"time": "1970-01-01T00:00:00.000000009Z"
|
||||
@@ -645,14 +624,8 @@ func Test_printPs(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
{
|
||||
v := flagJSON
|
||||
t.Cleanup(func() { flagJSON = v })
|
||||
flagJSON = tc.json
|
||||
}
|
||||
|
||||
output := new(strings.Builder)
|
||||
printPs(output, testTime, stubStore(tc.entries), tc.short)
|
||||
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)
|
||||
@@ -13,22 +13,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
fsuConfFile = "/etc/fsurc"
|
||||
envShim = "FORTIFY_SHIM"
|
||||
envAID = "FORTIFY_APP_ID"
|
||||
envGroups = "FORTIFY_GROUPS"
|
||||
hsuConfFile = "/etc/hsurc"
|
||||
envShim = "HAKUREI_SHIM"
|
||||
envAID = "HAKUREI_APP_ID"
|
||||
envGroups = "HAKUREI_GROUPS"
|
||||
|
||||
PR_SET_NO_NEW_PRIVS = 0x26
|
||||
)
|
||||
|
||||
var (
|
||||
Fmain = compPoison
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("fsu: ")
|
||||
log.SetPrefix("hsu: ")
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
@@ -40,20 +35,16 @@ func main() {
|
||||
log.Fatal("this program must not be started by root")
|
||||
}
|
||||
|
||||
var fmain string
|
||||
if p, ok := checkPath(Fmain); !ok {
|
||||
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
|
||||
} else {
|
||||
fmain = p
|
||||
}
|
||||
|
||||
var toolPath string
|
||||
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
||||
if p, err := os.Readlink(pexe); err != nil {
|
||||
log.Fatalf("cannot read parent executable path: %v", err)
|
||||
} else if strings.HasSuffix(p, " (deleted)") {
|
||||
log.Fatal("fortify executable has been deleted")
|
||||
} else if p != fmain {
|
||||
log.Fatal("this program must be started by fortify")
|
||||
log.Fatal("hakurei executable has been deleted")
|
||||
} else if p != mustCheckPath(hmain) {
|
||||
log.Fatal("this program must be started by hakurei")
|
||||
} else {
|
||||
toolPath = p
|
||||
}
|
||||
|
||||
// uid = 1000000 +
|
||||
@@ -61,27 +52,27 @@ func main() {
|
||||
// aid
|
||||
uid := 1000000
|
||||
|
||||
// refuse to run if fsurc is not protected correctly
|
||||
if s, err := os.Stat(fsuConfFile); err != nil {
|
||||
// refuse to run if hsurc is not protected correctly
|
||||
if s, err := os.Stat(hsuConfFile); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if s.Mode().Perm() != 0400 {
|
||||
log.Fatal("bad fsurc perm")
|
||||
log.Fatal("bad hsurc perm")
|
||||
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
|
||||
log.Fatal("fsurc must be owned by uid 0")
|
||||
log.Fatal("hsurc must be owned by uid 0")
|
||||
}
|
||||
|
||||
// authenticate before accepting user input
|
||||
if f, err := os.Open(fsuConfFile); err != nil {
|
||||
if f, err := os.Open(hsuConfFile); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if fid, ok := mustParseConfig(f, puid); !ok {
|
||||
log.Fatalf("uid %d is not in the fsurc file", puid)
|
||||
log.Fatalf("uid %d is not in the hsurc file", puid)
|
||||
} else {
|
||||
uid += fid * 10000
|
||||
}
|
||||
|
||||
// allowed aid range 0 to 9999
|
||||
if as, ok := os.LookupEnv(envAID); !ok {
|
||||
log.Fatal("FORTIFY_APP_ID not set")
|
||||
log.Fatal("HAKUREI_APP_ID not set")
|
||||
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
|
||||
log.Fatal("invalid aid")
|
||||
} else {
|
||||
@@ -91,12 +82,12 @@ func main() {
|
||||
// pass through setup fd to shim
|
||||
var shimSetupFd string
|
||||
if s, ok := os.LookupEnv(envShim); !ok {
|
||||
// fortify requests target uid
|
||||
// hakurei requests target uid
|
||||
// print resolved uid and exit
|
||||
fmt.Print(uid)
|
||||
os.Exit(0)
|
||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
||||
log.Fatal("FORTIFY_SHIM holds an invalid value")
|
||||
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
||||
} else {
|
||||
shimSetupFd = s
|
||||
}
|
||||
@@ -133,7 +124,7 @@ func main() {
|
||||
panic("uid out of bounds")
|
||||
}
|
||||
|
||||
// careful! users in the allowlist is effectively allowed to drop groups via fsu
|
||||
// careful! users in the allowlist is effectively allowed to drop groups via hsu
|
||||
|
||||
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
||||
log.Fatalf("cannot set gid: %v", err)
|
||||
@@ -147,13 +138,9 @@ func main() {
|
||||
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
||||
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||
}
|
||||
if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
log.Fatalf("cannot start shim: %v", err)
|
||||
}
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func checkPath(p string) (string, bool) {
|
||||
return p, p != compPoison && p != "" && path.IsAbs(p)
|
||||
}
|
||||
23
cmd/hsu/package.nix
Normal file
23
cmd/hsu/package.nix
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
lib,
|
||||
buildGoModule,
|
||||
hakurei ? abort "hakurei package required",
|
||||
}:
|
||||
|
||||
buildGoModule {
|
||||
pname = "${hakurei.pname}-hsu";
|
||||
inherit (hakurei) version;
|
||||
|
||||
src = ./.;
|
||||
inherit (hakurei) vendorHash;
|
||||
env.CGO_ENABLED = 0;
|
||||
|
||||
preBuild = ''
|
||||
go mod init hsu >& /dev/null
|
||||
'';
|
||||
|
||||
ldflags = lib.attrsets.foldlAttrs (
|
||||
ldflags: name: value:
|
||||
ldflags ++ [ "-X main.${name}=${value}" ]
|
||||
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; };
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
|
||||
if ok {
|
||||
// allowed fid range 0 to 99
|
||||
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
|
||||
return -1, false, fmt.Errorf("invalid fortify uid on line %d", line)
|
||||
return -1, false, fmt.Errorf("invalid identity on line %d", line)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func Test_parseConfig(t *testing.T) {
|
||||
{"empty", 0, -1, "", ``},
|
||||
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
||||
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
|
||||
{"invalid fid", 1000, -1, "invalid fortify uid on line 1", `1000 f`},
|
||||
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
|
||||
{"match", 1000, 0, "", `1000 0`},
|
||||
}
|
||||
|
||||
20
cmd/hsu/path.go
Normal file
20
cmd/hsu/path.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path"
|
||||
)
|
||||
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
|
||||
var (
|
||||
hmain = compPoison
|
||||
)
|
||||
|
||||
func mustCheckPath(p string) string {
|
||||
if p != compPoison && p != "" && path.IsAbs(p) {
|
||||
return p
|
||||
}
|
||||
log.Fatal("this program is compiled incorrectly")
|
||||
return compPoison
|
||||
}
|
||||
154
cmd/planterette/app.go
Normal file
154
cmd/planterette/app.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"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]
|
||||
Net bool `json:"net,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 system.Enablement `json:"enablements"`
|
||||
|
||||
// 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 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) *hst.Config {
|
||||
config := &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: argv[0],
|
||||
Args: argv,
|
||||
|
||||
Enablements: app.Enablements,
|
||||
|
||||
SystemBus: app.SystemBus,
|
||||
SessionBus: app.SessionBus,
|
||||
DirectWayland: app.DirectWayland,
|
||||
|
||||
Username: "hakurei",
|
||||
Shell: shellPath,
|
||||
Data: pathSet.homeDir,
|
||||
Dir: path.Join("/data/data", app.ID),
|
||||
|
||||
Identity: app.Identity,
|
||||
Groups: app.Groups,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name),
|
||||
Devel: app.Devel,
|
||||
Userns: app.Userns,
|
||||
Net: app.Net,
|
||||
Device: app.Device,
|
||||
Tty: app.Tty || flagDropShell,
|
||||
MapRealUID: app.MapRealUID,
|
||||
Filesystem: []*hst.FilesystemConfig{
|
||||
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
|
||||
{Src: pathSet.metaPath, Dst: path.Join(hst.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: []*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")
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
func formatHostname(name string) string {
|
||||
if h, err := os.Hostname(); err != nil {
|
||||
log.Printf("cannot get hostname: %v", err)
|
||||
return "hakurei-" + name
|
||||
} else {
|
||||
return h + "-" + name
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
closureInfo,
|
||||
writeScript,
|
||||
runtimeShell,
|
||||
writeText,
|
||||
@@ -15,18 +17,21 @@
|
||||
runCommand,
|
||||
fetchFromGitHub,
|
||||
|
||||
zstd,
|
||||
nix,
|
||||
sqlite,
|
||||
|
||||
name ? throw "name is required",
|
||||
version ? throw "version is required",
|
||||
pname ? "${name}-${version}",
|
||||
modules ? [ ],
|
||||
nixosModules ? [ ],
|
||||
script ? ''
|
||||
exec "$SHELL" "$@"
|
||||
'',
|
||||
|
||||
id ? name,
|
||||
app_id ? throw "app_id is required",
|
||||
identity ? throw "identity is required",
|
||||
groups ? [ ],
|
||||
userns ? false,
|
||||
net ? true,
|
||||
@@ -52,7 +57,7 @@ let
|
||||
modules = modules ++ [
|
||||
{
|
||||
home = {
|
||||
username = "fortify";
|
||||
username = "hakurei";
|
||||
homeDirectory = "/data/data/${id}";
|
||||
stateVersion = "22.11";
|
||||
};
|
||||
@@ -60,7 +65,7 @@ let
|
||||
];
|
||||
};
|
||||
|
||||
launcher = writeScript "fortify-${pname}" ''
|
||||
launcher = writeScript "hakurei-${pname}" ''
|
||||
#!${runtimeShell} -el
|
||||
${script}
|
||||
'';
|
||||
@@ -72,6 +77,8 @@ let
|
||||
etc.nixpkgs.source = nixpkgs.outPath;
|
||||
systemPackages = [ pkgs.nix ];
|
||||
};
|
||||
|
||||
imports = nixosModules;
|
||||
};
|
||||
nixos = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
@@ -140,7 +147,7 @@ let
|
||||
name
|
||||
version
|
||||
id
|
||||
app_id
|
||||
identity
|
||||
launcher
|
||||
groups
|
||||
userns
|
||||
@@ -164,11 +171,7 @@ let
|
||||
broadcast = { };
|
||||
});
|
||||
|
||||
enablements =
|
||||
(if allow_wayland then 1 else 0)
|
||||
+ (if allow_x11 then 2 else 0)
|
||||
+ (if allow_dbus then 4 else 0)
|
||||
+ (if allow_pulse then 8 else 0);
|
||||
enablements = (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);
|
||||
|
||||
mesa = if gpu then mesaWrappers else null;
|
||||
nix_gl = if gpu then nixGL else null;
|
||||
@@ -177,26 +180,73 @@ let
|
||||
};
|
||||
in
|
||||
|
||||
writeScript "fortify-${pname}-bundle-prelude" ''
|
||||
#!${runtimeShell} -el
|
||||
OUT="$(mktemp -d)"
|
||||
TAR="$(mktemp -u)"
|
||||
set -x
|
||||
stdenv.mkDerivation {
|
||||
name = "${pname}.pkg";
|
||||
inherit version;
|
||||
__structuredAttrs = true;
|
||||
|
||||
nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
|
||||
nix store --store "$OUT" optimise
|
||||
chmod -R +r "$OUT/nix/var"
|
||||
nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19¶llel-compression=true" \
|
||||
"${homeManagerConfiguration.activationPackage}" \
|
||||
"${launcher}" ${if gpu then "${mesaWrappers} ${nixGL}" else ""}
|
||||
mkdir -p "$OUT/etc"
|
||||
tar -C "$OUT/etc" -xf "${etc}/etc.tar"
|
||||
cp "${writeText "bundle.json" info}" "$OUT/bundle.json"
|
||||
nativeBuildInputs = [
|
||||
zstd
|
||||
nix
|
||||
sqlite
|
||||
];
|
||||
|
||||
# creating an intermediate file improves zstd performance
|
||||
tar -C "$OUT" -cf "$TAR" .
|
||||
chmod +w -R "$OUT" && rm -rf "$OUT"
|
||||
buildCommand = ''
|
||||
NIX_ROOT="$(mktemp -d)"
|
||||
export USER="nobody"
|
||||
|
||||
zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
|
||||
rm "$TAR"
|
||||
''
|
||||
# create bootstrap store
|
||||
bootstrapClosureInfo="${
|
||||
closureInfo {
|
||||
rootPaths = [
|
||||
nix
|
||||
nixos.config.system.build.toplevel
|
||||
];
|
||||
}
|
||||
}"
|
||||
echo "copying bootstrap store paths..."
|
||||
mkdir -p "$NIX_ROOT/nix/store"
|
||||
xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
|
||||
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
|
||||
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
|
||||
sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||
chmod -R +r "$NIX_ROOT/nix/var"
|
||||
|
||||
# create binary cache
|
||||
closureInfo="${
|
||||
closureInfo {
|
||||
rootPaths =
|
||||
[
|
||||
homeManagerConfiguration.activationPackage
|
||||
launcher
|
||||
]
|
||||
++ optionals gpu [
|
||||
mesaWrappers
|
||||
nixGL
|
||||
];
|
||||
}
|
||||
}"
|
||||
echo "copying application paths..."
|
||||
TMP_STORE="$(mktemp -d)"
|
||||
mkdir -p "$TMP_STORE/nix/store"
|
||||
xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
|
||||
NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
|
||||
sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||
NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
|
||||
--verbose --log-format raw-with-logs \
|
||||
copy --all --no-check-sigs --to \
|
||||
"file://$NIX_ROOT/res?compression=zstd&compression-level=19¶llel-compression=true"
|
||||
|
||||
# package /etc
|
||||
mkdir -p "$NIX_ROOT/etc"
|
||||
tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
|
||||
|
||||
# write metadata
|
||||
cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
|
||||
|
||||
# create an intermediate file to improve zstd performance
|
||||
INTER="$(mktemp)"
|
||||
tar -C "$NIX_ROOT" -cf "$INTER" .
|
||||
zstd -T0 -19 -fo "$out" "$INTER"
|
||||
'';
|
||||
}
|
||||
333
cmd/planterette/main.go
Normal file
333
cmd/planterette/main.go
Normal file
@@ -0,0 +1,333 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
const shellPath = "/run/current-system/sw/bin/bash"
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
)
|
||||
|
||||
func init() {
|
||||
hlog.Prepare("planterette")
|
||||
if err := os.Setenv("SHELL", shellPath); err != nil {
|
||||
log.Fatalf("cannot set $SHELL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagDropShell bool
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
|
||||
|
||||
{
|
||||
var (
|
||||
flagDropShellActivate bool
|
||||
)
|
||||
c.NewCommand("install", "Install an application from its package", func(args []string) error {
|
||||
if len(args) != 1 {
|
||||
log.Println("invalid argument")
|
||||
return syscall.EINVAL
|
||||
}
|
||||
pkgPath := args[0]
|
||||
if !path.IsAbs(pkgPath) {
|
||||
if dir, err := os.Getwd(); err != nil {
|
||||
log.Printf("cannot get current directory: %v", err)
|
||||
return err
|
||||
} else {
|
||||
pkgPath = path.Join(dir, pkgPath)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Look up paths to programs started by planterette.
|
||||
This is done here to ease error handling as cleanup is not yet required.
|
||||
*/
|
||||
|
||||
var (
|
||||
_ = lookPath("zstd")
|
||||
tar = lookPath("tar")
|
||||
chmod = lookPath("chmod")
|
||||
rm = lookPath("rm")
|
||||
)
|
||||
|
||||
/*
|
||||
Extract package and set up for cleanup.
|
||||
*/
|
||||
|
||||
var workDir string
|
||||
if p, err := os.MkdirTemp("", "planterette.*"); err != nil {
|
||||
log.Printf("cannot create temporary directory: %v", err)
|
||||
return err
|
||||
} else {
|
||||
workDir = p
|
||||
}
|
||||
cleanup := func() {
|
||||
// should be faster than a native implementation
|
||||
mustRun(chmod, "-R", "+w", workDir)
|
||||
mustRun(rm, "-rf", workDir)
|
||||
}
|
||||
beforeRunFail.Store(&cleanup)
|
||||
|
||||
mustRun(tar, "-C", workDir, "-xf", pkgPath)
|
||||
|
||||
/*
|
||||
Parse bundle and app metadata, do pre-install checks.
|
||||
*/
|
||||
|
||||
bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
|
||||
pathSet := pathSetByApp(bundle.ID)
|
||||
|
||||
a := bundle
|
||||
if s, err := os.Stat(pathSet.metaPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
cleanup()
|
||||
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
|
||||
return err
|
||||
}
|
||||
// did not modify app, clean installation condition met later
|
||||
} else if s.IsDir() {
|
||||
cleanup()
|
||||
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
||||
return syscall.EBADMSG
|
||||
} else {
|
||||
a = loadAppInfo(pathSet.metaPath, cleanup)
|
||||
if a.ID != bundle.ID {
|
||||
cleanup()
|
||||
log.Printf("app %q claims to have identifier %q",
|
||||
bundle.ID, a.ID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
// sec: should verify credentials
|
||||
}
|
||||
|
||||
if a != bundle {
|
||||
// do not try to re-install
|
||||
if a.NixGL == bundle.NixGL &&
|
||||
a.CurrentSystem == bundle.CurrentSystem &&
|
||||
a.Launcher == bundle.Launcher &&
|
||||
a.ActivationPackage == bundle.ActivationPackage {
|
||||
cleanup()
|
||||
log.Printf("package %q is identical to local application %q",
|
||||
pkgPath, a.ID)
|
||||
return errSuccess
|
||||
}
|
||||
|
||||
// identity determines uid
|
||||
if a.Identity != bundle.Identity {
|
||||
cleanup()
|
||||
log.Printf("package %q identity %d differs from installed %d",
|
||||
pkgPath, bundle.Identity, a.Identity)
|
||||
return syscall.EBADE
|
||||
}
|
||||
|
||||
// sec: should compare version string
|
||||
hlog.Verbosef("installing application %q version %q over local %q",
|
||||
bundle.ID, bundle.Version, a.Version)
|
||||
} else {
|
||||
hlog.Verbosef("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
/*
|
||||
Setup steps for files owned by the target user.
|
||||
*/
|
||||
|
||||
withCacheDir(ctx, "install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + hst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
"mkdir -p etc",
|
||||
"chmod -R +w etc",
|
||||
"rm -rf etc",
|
||||
"cp -dRf $BUNDLE/etc etc",
|
||||
// replace inner /nix
|
||||
"mkdir -p nix",
|
||||
"chmod -R +w nix",
|
||||
"rm -rf nix",
|
||||
"cp -dRf /nix nix",
|
||||
// copy from binary cache
|
||||
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
|
||||
// deduplicate nix store
|
||||
"nix store --offline --store $PWD optimise",
|
||||
// make cache directory world-readable for autoetc
|
||||
"chmod 0755 .",
|
||||
}, workDir, bundle, pathSet, flagDropShell, cleanup)
|
||||
|
||||
if bundle.GPU {
|
||||
withCacheDir(ctx, "mesa-wrappers", []string{
|
||||
// link nixGL mesa wrappers
|
||||
"mkdir -p nix/.nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
|
||||
}, workDir, bundle, pathSet, false, cleanup)
|
||||
}
|
||||
|
||||
/*
|
||||
Activate home-manager generation.
|
||||
*/
|
||||
|
||||
withNixDaemon(ctx, "activate", []string{
|
||||
// clean up broken links
|
||||
"mkdir -p .local/state/{nix,home-manager}",
|
||||
"chmod -R +w .local/state/{nix,home-manager}",
|
||||
"rm -rf .local/state/{nix,home-manager}",
|
||||
// run activation script
|
||||
bundle.ActivationPackage + "/activate",
|
||||
}, false, func(config *hst.Config) *hst.Config { return config },
|
||||
bundle, pathSet, flagDropShellActivate, cleanup)
|
||||
|
||||
/*
|
||||
Installation complete. Write metadata to block re-installs or downgrades.
|
||||
*/
|
||||
|
||||
// serialise metadata to ensure consistency
|
||||
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot create metadata file: %v", err)
|
||||
return err
|
||||
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot write metadata: %v", err)
|
||||
return err
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Printf("cannot close metadata file: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot rename metadata file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup()
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
|
||||
}
|
||||
|
||||
{
|
||||
var (
|
||||
flagDropShellNixGL bool
|
||||
flagAutoDrivers bool
|
||||
)
|
||||
c.NewCommand("start", "Start an application", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Println("invalid argument")
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
/*
|
||||
Parse app metadata.
|
||||
*/
|
||||
|
||||
id := args[0]
|
||||
pathSet := pathSetByApp(id)
|
||||
a := loadAppInfo(pathSet.metaPath, func() {})
|
||||
if a.ID != id {
|
||||
log.Printf("app %q claims to have identifier %q", id, a.ID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
|
||||
/*
|
||||
Prepare nixGL.
|
||||
*/
|
||||
|
||||
if a.GPU && flagAutoDrivers {
|
||||
withNixDaemon(ctx, "nix-gl", []string{
|
||||
"mkdir -p /nix/.nixGL/auto",
|
||||
"rm -rf /nix/.nixGL/auto",
|
||||
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/opengl " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + a.NixGL,
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + a.NixGL + "#nixVulkanNvidia",
|
||||
}, true, func(config *hst.Config) *hst.Config {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
|
||||
{Src: "/etc/resolv.conf"},
|
||||
{Src: "/sys/block"},
|
||||
{Src: "/sys/bus"},
|
||||
{Src: "/sys/class"},
|
||||
{Src: "/sys/dev"},
|
||||
{Src: "/sys/devices"},
|
||||
}...)
|
||||
appendGPUFilesystem(config)
|
||||
return config
|
||||
}, a, pathSet, flagDropShellNixGL, func() {})
|
||||
}
|
||||
|
||||
/*
|
||||
Create app configuration.
|
||||
*/
|
||||
|
||||
argv := make([]string, 1, len(args))
|
||||
if !flagDropShell {
|
||||
argv[0] = a.Launcher
|
||||
} else {
|
||||
argv[0] = shellPath
|
||||
}
|
||||
argv = append(argv, args[1:]...)
|
||||
|
||||
config := a.toFst(pathSet, argv, flagDropShell)
|
||||
|
||||
/*
|
||||
Expose GPU devices.
|
||||
*/
|
||||
|
||||
if a.GPU {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem,
|
||||
&hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")})
|
||||
appendGPUFilesystem(config)
|
||||
}
|
||||
|
||||
/*
|
||||
Spawn app.
|
||||
*/
|
||||
|
||||
mustRunApp(ctx, config, func() {})
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
|
||||
Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
|
||||
}
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
101
cmd/planterette/paths.go
Normal file
101
cmd/planterette/paths.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var (
|
||||
dataHome string
|
||||
)
|
||||
|
||||
func init() {
|
||||
// dataHome
|
||||
if p, ok := os.LookupEnv("HAKUREI_DATA_HOME"); ok {
|
||||
dataHome = p
|
||||
} else {
|
||||
dataHome = "/var/lib/hakurei/" + 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) {
|
||||
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 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 *hst.Config) {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.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,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -8,33 +9,27 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
var hakureiPath = internal.MustHakureiPath()
|
||||
|
||||
var (
|
||||
Fmain = compPoison
|
||||
)
|
||||
|
||||
func fortifyApp(config *fst.Config, beforeFail func()) {
|
||||
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
st io.WriteCloser
|
||||
)
|
||||
if p, ok := internal.Path(Fmain); !ok {
|
||||
beforeFail()
|
||||
log.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
|
||||
} else if r, w, err := os.Pipe(); err != nil {
|
||||
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot pipe: %v", err)
|
||||
} else {
|
||||
if fmsg.Load() {
|
||||
cmd = exec.Command(p, "-v", "app", "3")
|
||||
if hlog.Load() {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
|
||||
} else {
|
||||
cmd = exec.Command(p, "app", "3")
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
|
||||
}
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.ExtraFiles = []*os.File{r}
|
||||
@@ -50,7 +45,7 @@ func fortifyApp(config *fst.Config, beforeFail func()) {
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot start fortify: %v", err)
|
||||
log.Fatalf("cannot start hakurei: %v", err)
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
62
cmd/planterette/test/configuration.nix
Normal file
62
cmd/planterette/test/configuration.nix
Normal file
@@ -0,0 +1,62 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.alice.home.stateVersion = "24.11";
|
||||
|
||||
# Automatically login on tty1 as a normal user:
|
||||
services.getty.autologinUser = "alice";
|
||||
|
||||
environment = {
|
||||
variables = {
|
||||
SWAYSOCK = "/tmp/sway-ipc.sock";
|
||||
WLR_RENDERER = "pixman";
|
||||
};
|
||||
};
|
||||
|
||||
# Automatically configure and start Sway when logging in on tty1:
|
||||
programs.bash.loginShellInit = ''
|
||||
if [ "$(tty)" = "/dev/tty1" ]; then
|
||||
set -e
|
||||
|
||||
mkdir -p ~/.config/sway
|
||||
(sed s/Mod4/Mod1/ /etc/sway/config &&
|
||||
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
|
||||
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
|
||||
|
||||
sway --validate
|
||||
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
|
||||
fi
|
||||
'';
|
||||
|
||||
programs.sway.enable = true;
|
||||
|
||||
virtualisation = {
|
||||
diskSize = 6 * 1024;
|
||||
|
||||
qemu.options = [
|
||||
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
|
||||
"-vga none -device virtio-gpu-pci"
|
||||
|
||||
# Increase zstd performance:
|
||||
"-smp 8"
|
||||
];
|
||||
};
|
||||
|
||||
environment.hakurei = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/hakurei";
|
||||
users.alice = 0;
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
};
|
||||
}
|
||||
34
cmd/planterette/test/default.nix
Normal file
34
cmd/planterette/test/default.nix
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
nixosTest,
|
||||
callPackage,
|
||||
|
||||
system,
|
||||
self,
|
||||
}:
|
||||
let
|
||||
buildPackage = self.buildPackage.${system};
|
||||
in
|
||||
nixosTest {
|
||||
name = "planterette";
|
||||
nodes.machine = {
|
||||
environment.etc = {
|
||||
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
|
||||
};
|
||||
|
||||
imports = [
|
||||
./configuration.nix
|
||||
|
||||
self.nixosModules.hakurei
|
||||
self.inputs.home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
|
||||
# adapted from nixos sway integration tests
|
||||
|
||||
# testScriptWithTypes:49: error: Cannot call function of unknown type
|
||||
# (machine.succeed if succeed else machine.execute)(
|
||||
# ^
|
||||
# Found 1 error in 1 file (checked 1 source file)
|
||||
skipTypeCheck = true;
|
||||
testScript = builtins.readFile ./test.py;
|
||||
}
|
||||
48
cmd/planterette/test/foot.nix
Normal file
48
cmd/planterette/test/foot.nix
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
lib,
|
||||
buildPackage,
|
||||
foot,
|
||||
wayland-utils,
|
||||
inconsolata,
|
||||
}:
|
||||
|
||||
buildPackage {
|
||||
name = "foot";
|
||||
inherit (foot) version;
|
||||
|
||||
identity = 2;
|
||||
id = "org.codeberg.dnkl.foot";
|
||||
|
||||
modules = [
|
||||
{
|
||||
home.packages = [
|
||||
foot
|
||||
|
||||
# For wayland-info:
|
||||
wayland-utils
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
nixosModules = [
|
||||
{
|
||||
# To help with OCR:
|
||||
environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
|
||||
main = {
|
||||
font = "inconsolata:size=14";
|
||||
};
|
||||
colors = rec {
|
||||
foreground = "000000";
|
||||
background = "ffffff";
|
||||
regular2 = foreground;
|
||||
};
|
||||
};
|
||||
|
||||
fonts.packages = [ inconsolata ];
|
||||
}
|
||||
];
|
||||
|
||||
script = ''
|
||||
exec foot "$@"
|
||||
'';
|
||||
}
|
||||
108
cmd/planterette/test/test.py
Normal file
108
cmd/planterette/test/test.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import json
|
||||
import shlex
|
||||
|
||||
q = shlex.quote
|
||||
NODE_GROUPS = ["nodes", "floating_nodes"]
|
||||
|
||||
|
||||
def swaymsg(command: str = "", succeed=True, type="command"):
|
||||
assert command != "" or type != "command", "Must specify command or type"
|
||||
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
|
||||
with machine.nested(
|
||||
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
|
||||
):
|
||||
ret = (machine.succeed if succeed else machine.execute)(
|
||||
f"su - alice -c {shell}"
|
||||
)
|
||||
|
||||
# execute also returns a status code, but disregard.
|
||||
if not succeed:
|
||||
_, ret = ret
|
||||
|
||||
if not succeed and not ret:
|
||||
return None
|
||||
|
||||
parsed = json.loads(ret)
|
||||
return parsed
|
||||
|
||||
|
||||
def walk(tree):
|
||||
yield tree
|
||||
for group in NODE_GROUPS:
|
||||
for node in tree.get(group, []):
|
||||
yield from walk(node)
|
||||
|
||||
|
||||
def wait_for_window(pattern):
|
||||
def func(last_chance):
|
||||
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
|
||||
|
||||
if last_chance:
|
||||
nodes = list(nodes)
|
||||
machine.log(f"Last call! Current list of windows: {nodes}")
|
||||
|
||||
return any(pattern in name for name in nodes)
|
||||
|
||||
retry(func)
|
||||
|
||||
|
||||
def collect_state_ui(name):
|
||||
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.ps", "")
|
||||
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.json", "")
|
||||
machine.screenshot(name)
|
||||
|
||||
|
||||
def check_state(name, enablements):
|
||||
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
|
||||
if len(instances) != 1:
|
||||
raise Exception(f"unexpected state length {len(instances)}")
|
||||
instance = next(iter(instances.values()))
|
||||
|
||||
config = instance['config']
|
||||
|
||||
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
|
||||
raise Exception(f"unexpected args {instance['config']['args']}")
|
||||
|
||||
if config['enablements'] != enablements:
|
||||
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
|
||||
|
||||
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check hakurei's version:
|
||||
print(machine.succeed("sudo -u alice -i hakurei version"))
|
||||
|
||||
# Wait for Sway to complete startup:
|
||||
machine.wait_for_file("/run/user/1000/wayland-1")
|
||||
machine.wait_for_file("/tmp/sway-ipc.sock")
|
||||
|
||||
# Prepare planterette directory:
|
||||
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
|
||||
|
||||
# Install planterette app:
|
||||
swaymsg("exec planterette -v install /etc/foot.pkg && touch /tmp/planterette-install-ok")
|
||||
machine.wait_for_file("/tmp/planterette-install-ok")
|
||||
|
||||
# Start app (foot) with Wayland enablement:
|
||||
swaymsg("exec planterette -v start org.codeberg.dnkl.foot")
|
||||
wait_for_window("hakurei@machine-foot")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||
machine.wait_for_file("/tmp/hakurei.1000/tmpdir/2/success-client")
|
||||
collect_state_ui("app_wayland")
|
||||
check_state("foot", 13)
|
||||
# Verify acl on XDG_RUNTIME_DIR:
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
swaymsg("exit", succeed=False)
|
||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||
|
||||
# Print hakurei runDir contents:
|
||||
print(machine.succeed("find /run/user/1000/hakurei"))
|
||||
114
cmd/planterette/with.go
Normal file
114
cmd/planterette/with.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"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: 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",
|
||||
},
|
||||
|
||||
Username: "hakurei",
|
||||
Shell: shellPath,
|
||||
Data: pathSet.homeDir,
|
||||
Dir: path.Join("/data/data", 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
|
||||
Net: net,
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []*hst.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,
|
||||
},
|
||||
}), dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func withCacheDir(
|
||||
ctx context.Context,
|
||||
action string, command []string, workDir string,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
mustRunAppDropShell(ctx, &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: shellPath,
|
||||
Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
|
||||
|
||||
Username: "nixos",
|
||||
Shell: shellPath,
|
||||
Data: pathSet.cacheDir, // this also ensures cacheDir via shim
|
||||
Dir: path.Join("/data/data", 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.FilesystemConfig{
|
||||
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
|
||||
{Src: workDir, Dst: path.Join(hst.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,
|
||||
},
|
||||
}, dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
|
||||
if dropShell {
|
||||
config.Args = []string{shellPath, "-l"}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
beforeFail()
|
||||
internal.Exit(0)
|
||||
}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
}
|
||||
65
command/builder.go
Normal file
65
command/builder.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New initialises a root Node.
|
||||
func New(output io.Writer, logf LogFunc, name string, early HandlerFunc) Command {
|
||||
c := rootNode{newNode(output, logf, name, "")}
|
||||
c.f = early
|
||||
return c
|
||||
}
|
||||
|
||||
func newNode(output io.Writer, logf LogFunc, name, usage string) *node {
|
||||
n := &node{
|
||||
name: name, usage: usage,
|
||||
out: output, logf: logf,
|
||||
set: flag.NewFlagSet(name, flag.ContinueOnError),
|
||||
}
|
||||
n.set.SetOutput(output)
|
||||
n.set.Usage = func() {
|
||||
_ = n.writeHelp()
|
||||
if n.suffix.Len() > 0 {
|
||||
_, _ = fmt.Fprintln(output, "Flags:")
|
||||
n.set.PrintDefaults()
|
||||
_, _ = fmt.Fprintln(output)
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *node) Command(name, usage string, f HandlerFunc) Node {
|
||||
n.NewCommand(name, usage, f)
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *node) NewCommand(name, usage string, f HandlerFunc) Flag[Node] {
|
||||
if f == nil {
|
||||
panic("invalid handler")
|
||||
}
|
||||
if name == "" || usage == "" {
|
||||
panic("invalid subcommand")
|
||||
}
|
||||
|
||||
s := newNode(n.out, n.logf, name, usage)
|
||||
s.f = f
|
||||
if !n.adopt(s) {
|
||||
panic("attempted to initialise subcommand with non-unique name")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (n *node) New(name, usage string) Node {
|
||||
if name == "" || usage == "" {
|
||||
panic("invalid subcommand tree")
|
||||
}
|
||||
s := newNode(n.out, n.logf, name, usage)
|
||||
if !n.adopt(s) {
|
||||
panic("attempted to initialise subcommand tree with non-unique name")
|
||||
}
|
||||
return s
|
||||
}
|
||||
56
command/builder_test.go
Normal file
56
command/builder_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
c := command.New(nil, nil, "test", nil)
|
||||
stubHandler := func([]string) error { panic("unreachable") }
|
||||
|
||||
t.Run("nil direct handler", func(t *testing.T) {
|
||||
defer checkRecover(t, "Command", "invalid handler")
|
||||
c.Command("name", "usage", nil)
|
||||
})
|
||||
|
||||
t.Run("direct zero length", func(t *testing.T) {
|
||||
wantPanic := "invalid subcommand"
|
||||
t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("", "usage", stubHandler) })
|
||||
t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("name", "", stubHandler) })
|
||||
})
|
||||
|
||||
t.Run("direct adopt unique names", func(t *testing.T) {
|
||||
c.Command("d0", "usage", stubHandler)
|
||||
c.Command("d1", "usage", stubHandler)
|
||||
})
|
||||
|
||||
t.Run("direct adopt non-unique name", func(t *testing.T) {
|
||||
defer checkRecover(t, "Command", "attempted to initialise subcommand with non-unique name")
|
||||
c.Command("d0", "usage", stubHandler)
|
||||
})
|
||||
|
||||
t.Run("zero length", func(t *testing.T) {
|
||||
wantPanic := "invalid subcommand tree"
|
||||
t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("", "usage") })
|
||||
t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("name", "") })
|
||||
})
|
||||
|
||||
t.Run("direct adopt unique names", func(t *testing.T) {
|
||||
c.New("t0", "usage")
|
||||
c.New("t1", "usage")
|
||||
})
|
||||
|
||||
t.Run("direct adopt non-unique name", func(t *testing.T) {
|
||||
defer checkRecover(t, "Command", "attempted to initialise subcommand tree with non-unique name")
|
||||
c.New("t0", "usage")
|
||||
})
|
||||
}
|
||||
|
||||
func checkRecover(t *testing.T, name, wantPanic string) {
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("%s: panic = %v; wantPanic %v",
|
||||
name, r, wantPanic)
|
||||
}
|
||||
}
|
||||
55
command/command.go
Normal file
55
command/command.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Package command implements generic nested command parsing.
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UsageInternal causes the command to be hidden from help text when set as the usage string.
|
||||
const UsageInternal = "internal"
|
||||
|
||||
type (
|
||||
// HandlerFunc is called when matching a directly handled subcommand tree.
|
||||
HandlerFunc = func(args []string) error
|
||||
|
||||
// LogFunc is the function signature of a printf function.
|
||||
LogFunc = func(format string, a ...any)
|
||||
|
||||
// FlagDefiner is a deferred flag definer value, usually encapsulating the default value.
|
||||
FlagDefiner interface {
|
||||
// Define defines the flag in set.
|
||||
Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string)
|
||||
}
|
||||
|
||||
Flag[T any] interface {
|
||||
// Flag defines a generic flag type in Node's flag set.
|
||||
Flag(p any, name string, value FlagDefiner, usage string) T
|
||||
}
|
||||
|
||||
Command interface {
|
||||
Parse(arguments []string) error
|
||||
|
||||
// MustParse determines exit outcomes for Parse errors
|
||||
// and calls handleError if [HandlerFunc] returns a non-nil error.
|
||||
MustParse(arguments []string, handleError func(error))
|
||||
|
||||
baseNode[Command]
|
||||
}
|
||||
Node baseNode[Node]
|
||||
|
||||
baseNode[T any] interface {
|
||||
// Command appends a subcommand with direct command handling.
|
||||
Command(name, usage string, f HandlerFunc) T
|
||||
|
||||
// New returns a new subcommand tree.
|
||||
New(name, usage string) (sub Node)
|
||||
// NewCommand returns a new subcommand with direct command handling.
|
||||
NewCommand(name, usage string, f HandlerFunc) (sub Flag[Node])
|
||||
|
||||
// PrintHelp prints a help message to the configured writer.
|
||||
PrintHelp()
|
||||
|
||||
Flag[T]
|
||||
}
|
||||
)
|
||||
77
command/flag.go
Normal file
77
command/flag.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FlagError wraps errors returned by [flag].
|
||||
type FlagError struct{ error }
|
||||
|
||||
func (e FlagError) Success() bool { return errors.Is(e.error, flag.ErrHelp) }
|
||||
func (e FlagError) Is(target error) bool {
|
||||
return (e.error == nil && target == nil) ||
|
||||
((e.error != nil && target != nil) && e.error.Error() == target.Error())
|
||||
}
|
||||
|
||||
func (n *node) Flag(p any, name string, value FlagDefiner, usage string) Node {
|
||||
value.Define(&n.suffix, n.set, p, name, usage)
|
||||
return n
|
||||
}
|
||||
|
||||
// StringFlag is the default value of a string flag.
|
||||
type StringFlag string
|
||||
|
||||
func (v StringFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
|
||||
set.StringVar(p.(*string), name, string(v), usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + " <value>]")
|
||||
}
|
||||
|
||||
// IntFlag is the default value of an int flag.
|
||||
type IntFlag int
|
||||
|
||||
func (v IntFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
|
||||
set.IntVar(p.(*int), name, int(v), usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + " <int>]")
|
||||
}
|
||||
|
||||
// BoolFlag is the default value of a bool flag.
|
||||
type BoolFlag bool
|
||||
|
||||
func (v BoolFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
|
||||
set.BoolVar(p.(*bool), name, bool(v), usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + "]")
|
||||
}
|
||||
|
||||
// RepeatableFlag implements an ordered, repeatable string flag.
|
||||
type RepeatableFlag []string
|
||||
|
||||
func (r *RepeatableFlag) String() string {
|
||||
if r == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return strings.Join(*r, " ")
|
||||
}
|
||||
|
||||
func (r *RepeatableFlag) Set(v string) error {
|
||||
*r = append(*r, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepeatableFlag) Define(b *strings.Builder, set *flag.FlagSet, _ any, name, usage string) {
|
||||
set.Var(r, name, usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + " <value>]")
|
||||
}
|
||||
|
||||
// this has no effect on parse outcome
|
||||
func prettyFlag(name string) string {
|
||||
switch len(name) {
|
||||
case 0:
|
||||
panic("zero length flag name")
|
||||
case 1:
|
||||
return "-" + name
|
||||
default:
|
||||
return "--" + name
|
||||
}
|
||||
}
|
||||
53
command/help.go
Normal file
53
command/help.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
var ErrHelp = errors.New("help requested")
|
||||
|
||||
func (n *node) PrintHelp() { _ = n.writeHelp() }
|
||||
|
||||
func (n *node) writeHelp() error {
|
||||
if _, err := fmt.Fprintf(n.out,
|
||||
"\nUsage:\t%s [-h | --help]%s COMMAND [OPTIONS]\n",
|
||||
strings.Join(append(n.prefix, n.name), " "), &n.suffix,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if n.child != nil {
|
||||
if _, err := fmt.Fprint(n.out, "\nCommands:\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(n.out, 0, 1, 4, ' ', 0)
|
||||
if err := n.child.writeCommands(tw); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := n.out.Write([]byte{'\n'})
|
||||
if err == nil {
|
||||
err = ErrHelp
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *node) writeCommands(w io.Writer) error {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if n.usage != UsageInternal {
|
||||
if _, err := fmt.Fprintf(w, "\t%s\t%s\n", n.name, n.usage); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return n.next.writeCommands(w)
|
||||
}
|
||||
40
command/node.go
Normal file
40
command/node.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
child, next *node
|
||||
name, usage string
|
||||
|
||||
out io.Writer
|
||||
logf LogFunc
|
||||
|
||||
prefix []string
|
||||
suffix strings.Builder
|
||||
|
||||
f HandlerFunc
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
func (n *node) adopt(v *node) bool {
|
||||
if n.child != nil {
|
||||
return n.child.append(v)
|
||||
}
|
||||
n.child = v
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *node) append(v *node) bool {
|
||||
if n.name == v.name {
|
||||
return false
|
||||
}
|
||||
if n.next != nil {
|
||||
return n.next.append(v)
|
||||
}
|
||||
n.next = v
|
||||
return true
|
||||
}
|
||||
105
command/parse.go
Normal file
105
command/parse.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyTree = errors.New("subcommand tree has no nodes")
|
||||
ErrNoMatch = errors.New("did not match any subcommand")
|
||||
)
|
||||
|
||||
func (n *node) Parse(arguments []string) error {
|
||||
if n.usage == "" { // root node has zero length usage
|
||||
if n.next != nil {
|
||||
panic("invalid toplevel state")
|
||||
}
|
||||
goto match
|
||||
}
|
||||
|
||||
if len(arguments) == 0 {
|
||||
// unreachable: zero length args cause upper level to return with a help message
|
||||
panic("attempted to parse with zero length args")
|
||||
}
|
||||
if arguments[0] != n.name {
|
||||
if n.next == nil {
|
||||
n.printf("%q is not a valid command", arguments[0])
|
||||
return ErrNoMatch
|
||||
}
|
||||
n.next.prefix = n.prefix
|
||||
return n.next.Parse(arguments)
|
||||
}
|
||||
arguments = arguments[1:]
|
||||
|
||||
match:
|
||||
if n.child != nil {
|
||||
// propagate help prefix early: flag set usage dereferences help
|
||||
n.child.prefix = append(n.prefix, n.name)
|
||||
}
|
||||
|
||||
if n.set.Parsed() {
|
||||
panic("invalid set state")
|
||||
}
|
||||
if err := n.set.Parse(arguments); err != nil {
|
||||
return FlagError{err}
|
||||
}
|
||||
args := n.set.Args()
|
||||
|
||||
if n.child != nil {
|
||||
if n.f != nil {
|
||||
if n.usage != "" { // root node early special case
|
||||
panic("invalid subcommand tree state")
|
||||
}
|
||||
|
||||
// special case: root node calls HandlerFunc for initialisation
|
||||
if err := n.f(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return n.writeHelp()
|
||||
}
|
||||
return n.child.Parse(args)
|
||||
}
|
||||
|
||||
if n.f == nil {
|
||||
n.printf("%q has no subcommands", n.name)
|
||||
return ErrEmptyTree
|
||||
}
|
||||
return n.f(args)
|
||||
}
|
||||
|
||||
func (n *node) printf(format string, a ...any) {
|
||||
if n.logf == nil {
|
||||
log.Printf(format, a...)
|
||||
} else {
|
||||
n.logf(format, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) MustParse(arguments []string, handleError func(error)) {
|
||||
switch err := n.Parse(arguments); err {
|
||||
case nil:
|
||||
return
|
||||
case ErrHelp:
|
||||
os.Exit(0)
|
||||
case ErrNoMatch:
|
||||
os.Exit(1)
|
||||
case ErrEmptyTree:
|
||||
os.Exit(1)
|
||||
default:
|
||||
var flagError FlagError
|
||||
if !errors.As(err, &flagError) { // returned by HandlerFunc
|
||||
handleError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if flagError.Success() {
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
344
command/parse_test.go
Normal file
344
command/parse_test.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
buildTree func(wout, wlog io.Writer) command.Command
|
||||
args []string
|
||||
want string
|
||||
wantLog string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"d=0 empty sub",
|
||||
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
|
||||
[]string{""},
|
||||
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=0 empty sub garbage",
|
||||
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=0 no match",
|
||||
buildTestCommand,
|
||||
[]string{"nonexistent"},
|
||||
"", "test: \"nonexistent\" is not a valid command\n", command.ErrNoMatch,
|
||||
},
|
||||
{
|
||||
"d=0 direct error",
|
||||
buildTestCommand,
|
||||
[]string{"error"},
|
||||
"", "", errSuccess,
|
||||
},
|
||||
{
|
||||
"d=0 direct error garbage",
|
||||
buildTestCommand,
|
||||
[]string{"error", "0", "1", "2"},
|
||||
"", "", errSuccess,
|
||||
},
|
||||
{
|
||||
"d=0 direct success out of order",
|
||||
buildTestCommand,
|
||||
[]string{"succeed"},
|
||||
"", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 direct success output",
|
||||
buildTestCommand,
|
||||
[]string{"print", "0", "1", "2"},
|
||||
"012", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 out of order string flag",
|
||||
buildTestCommand,
|
||||
[]string{"string", "--string", "64d3b4b7b21788585845060e2199a78f"},
|
||||
"flag provided but not defined: -string\n\nUsage:\ttest string [-h | --help] COMMAND [OPTIONS]\n\n", "",
|
||||
errors.New("flag provided but not defined: -string"),
|
||||
},
|
||||
{
|
||||
"d=0 string flag",
|
||||
buildTestCommand,
|
||||
[]string{"--string", "64d3b4b7b21788585845060e2199a78f", "string"},
|
||||
"64d3b4b7b21788585845060e2199a78f", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 int flag",
|
||||
buildTestCommand,
|
||||
[]string{"--int", "2147483647", "int"},
|
||||
"2147483647", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 repeat flag",
|
||||
buildTestCommand,
|
||||
[]string{"--repeat", "0", "--repeat", "1", "--repeat", "2", "--repeat", "3", "--repeat", "4", "repeat"},
|
||||
"[0 1 2 3 4]", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 bool flag",
|
||||
buildTestCommand,
|
||||
[]string{"-v", "succeed"},
|
||||
"", "test: verbose\n", nil,
|
||||
},
|
||||
{
|
||||
"d=0 bool flag early error",
|
||||
buildTestCommand,
|
||||
[]string{"--fail", "succeed"},
|
||||
"", "", errSuccess,
|
||||
},
|
||||
|
||||
{
|
||||
"d=1 empty sub",
|
||||
buildTestCommand,
|
||||
[]string{"empty"},
|
||||
"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=1 empty sub garbage",
|
||||
buildTestCommand,
|
||||
[]string{"empty", "a", "b", "c", "d"},
|
||||
"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=1 empty sub help",
|
||||
buildTestCommand,
|
||||
[]string{"empty", "-h"},
|
||||
"\nUsage:\ttest empty [-h | --help] COMMAND [OPTIONS]\n\n", "", flag.ErrHelp,
|
||||
},
|
||||
{
|
||||
"d=1 no match",
|
||||
buildTestCommand,
|
||||
[]string{"join", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
|
||||
"", "test: \"23aa3bb0\" is not a valid command\n", command.ErrNoMatch,
|
||||
},
|
||||
{
|
||||
"d=1 direct success out",
|
||||
buildTestCommand,
|
||||
[]string{"join", "out", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
|
||||
"23aa3bb0, 34986782, d8859355, cd9ac317", "", nil,
|
||||
},
|
||||
{
|
||||
"d=1 direct success log",
|
||||
buildTestCommand,
|
||||
[]string{"join", "log", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
|
||||
"", "test: 23aa3bb0, 34986782, d8859355, cd9ac317\n", nil,
|
||||
},
|
||||
|
||||
{
|
||||
"d=4 empty sub",
|
||||
buildTestCommand,
|
||||
[]string{"deep", "d=2", "d=3", "d=4"},
|
||||
"", "test: \"d=4\" has no subcommands\n", command.ErrEmptyTree},
|
||||
|
||||
{
|
||||
"d=0 help",
|
||||
buildTestCommand,
|
||||
[]string{},
|
||||
`
|
||||
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
error return an error
|
||||
print wraps Fprint
|
||||
string print string passed by flag
|
||||
int print int passed by flag
|
||||
repeat print repeated values passed by flag
|
||||
empty empty subcommand
|
||||
join wraps strings.Join
|
||||
succeed this command succeeds
|
||||
deep top level of command tree with various levels
|
||||
|
||||
`, "", command.ErrHelp,
|
||||
},
|
||||
{
|
||||
"d=0 help flag",
|
||||
buildTestCommand,
|
||||
[]string{"-h"},
|
||||
`
|
||||
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
error return an error
|
||||
print wraps Fprint
|
||||
string print string passed by flag
|
||||
int print int passed by flag
|
||||
repeat print repeated values passed by flag
|
||||
empty empty subcommand
|
||||
join wraps strings.Join
|
||||
succeed this command succeeds
|
||||
deep top level of command tree with various levels
|
||||
|
||||
Flags:
|
||||
-fail
|
||||
fail early
|
||||
-int int
|
||||
store value for the "int" command (default -1)
|
||||
-repeat value
|
||||
store value for the "repeat" command
|
||||
-string string
|
||||
store value for the "string" command (default "default")
|
||||
-v verbose output
|
||||
|
||||
`, "", flag.ErrHelp,
|
||||
},
|
||||
|
||||
{
|
||||
"d=1 help",
|
||||
buildTestCommand,
|
||||
[]string{"join"},
|
||||
`
|
||||
Usage: test join [-h | --help] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
out write result to wout
|
||||
log log result to wlog
|
||||
|
||||
`, "", command.ErrHelp,
|
||||
},
|
||||
{
|
||||
"d=1 help flag",
|
||||
buildTestCommand,
|
||||
[]string{"join", "-h"},
|
||||
`
|
||||
Usage: test join [-h | --help] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
out write result to wout
|
||||
log log result to wlog
|
||||
|
||||
`, "", flag.ErrHelp,
|
||||
},
|
||||
|
||||
{
|
||||
"d=2 help",
|
||||
buildTestCommand,
|
||||
[]string{"deep", "d=2"},
|
||||
`
|
||||
Usage: test deep d=2 [-h | --help] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
d=3 relative third level
|
||||
|
||||
`, "", command.ErrHelp,
|
||||
},
|
||||
{
|
||||
"d=2 help flag",
|
||||
buildTestCommand,
|
||||
[]string{"deep", "d=2", "-h"},
|
||||
`
|
||||
Usage: test deep d=2 [-h | --help] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
d=3 relative third level
|
||||
|
||||
`, "", flag.ErrHelp,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
|
||||
c := tc.buildTree(wout, wlog)
|
||||
|
||||
if err := c.Parse(tc.args); !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Parse: error = %v; wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
if got := wout.String(); got != tc.want {
|
||||
t.Errorf("Parse: %s want %s", got, tc.want)
|
||||
}
|
||||
if gotLog := wlog.String(); gotLog != tc.wantLog {
|
||||
t.Errorf("Parse: log = %s wantLog %s", gotLog, tc.wantLog)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
errJoinLen = errors.New("not enough arguments to join")
|
||||
errSuccess = errors.New("success")
|
||||
)
|
||||
|
||||
func buildTestCommand(wout, wlog io.Writer) (c command.Command) {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagFail bool
|
||||
|
||||
flagString string
|
||||
flagInt int
|
||||
flagRepeat command.RepeatableFlag
|
||||
)
|
||||
|
||||
logf := newLogFunc(wlog)
|
||||
c = command.New(wout, logf, "test", func([]string) error {
|
||||
if flagVerbose {
|
||||
logf("verbose")
|
||||
}
|
||||
if flagFail {
|
||||
return errSuccess
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "verbose output").
|
||||
Flag(&flagFail, "fail", command.BoolFlag(false), "fail early").
|
||||
Command("error", "return an error", func([]string) error {
|
||||
return errSuccess
|
||||
}).
|
||||
Command("print", "wraps Fprint", func(args []string) error {
|
||||
a := make([]any, len(args))
|
||||
for i, v := range args {
|
||||
a[i] = v
|
||||
}
|
||||
_, err := fmt.Fprint(wout, a...)
|
||||
return err
|
||||
}).
|
||||
Flag(&flagString, "string", command.StringFlag("default"), "store value for the \"string\" command").
|
||||
Command("string", "print string passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagString); return err }).
|
||||
Flag(&flagInt, "int", command.IntFlag(-1), "store value for the \"int\" command").
|
||||
Command("int", "print int passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagInt); return err }).
|
||||
Flag(nil, "repeat", &flagRepeat, "store value for the \"repeat\" command").
|
||||
Command("repeat", "print repeated values passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagRepeat); return err })
|
||||
|
||||
c.New("empty", "empty subcommand")
|
||||
c.New("hidden", command.UsageInternal)
|
||||
|
||||
c.New("join", "wraps strings.Join").
|
||||
Command("out", "write result to wout", func(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errJoinLen
|
||||
}
|
||||
_, err := fmt.Fprint(wout, strings.Join(args[:len(args)-1], args[len(args)-1]))
|
||||
return err
|
||||
}).
|
||||
Command("log", "log result to wlog", func(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errJoinLen
|
||||
}
|
||||
logf("%s", strings.Join(args[:len(args)-1], args[len(args)-1]))
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Command("succeed", "this command succeeds", func([]string) error { return nil })
|
||||
|
||||
c.New("deep", "top level of command tree with various levels").
|
||||
New("d=2", "relative second level").
|
||||
New("d=3", "relative third level").
|
||||
New("d=4", "relative fourth level")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func newLogFunc(w io.Writer) command.LogFunc { return log.New(w, "test: ", 0).Printf }
|
||||
54
command/unreachable_test.go
Normal file
54
command/unreachable_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseUnreachable(t *testing.T) {
|
||||
// top level bypasses name matching and recursive calls to Parse
|
||||
// returns when encountering zero-length args
|
||||
t.Run("zero-length args", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
|
||||
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
|
||||
})
|
||||
|
||||
// top level must not have siblings
|
||||
t.Run("toplevel siblings", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "invalid toplevel state")
|
||||
n := newNode(panicWriter{}, nil, " ", "")
|
||||
n.append(newNode(panicWriter{}, nil, " ", " "))
|
||||
_ = n.Parse(nil)
|
||||
})
|
||||
|
||||
// a node with descendents must not have a direct handler
|
||||
t.Run("sub handle conflict", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "invalid subcommand tree state")
|
||||
n := newNode(panicWriter{}, nil, " ", " ")
|
||||
n.adopt(newNode(panicWriter{}, nil, " ", " "))
|
||||
n.f = func([]string) error { panic("unreachable") }
|
||||
_ = n.Parse([]string{" "})
|
||||
})
|
||||
|
||||
// this would only happen if a node was matched twice
|
||||
t.Run("parsed flag set", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "invalid set state")
|
||||
n := newNode(panicWriter{}, nil, " ", "")
|
||||
set := flag.NewFlagSet("parsed", flag.ContinueOnError)
|
||||
set.SetOutput(panicWriter{})
|
||||
_ = set.Parse(nil)
|
||||
n.set = set
|
||||
_ = n.Parse(nil)
|
||||
})
|
||||
}
|
||||
|
||||
type panicWriter struct{}
|
||||
|
||||
func (p panicWriter) Write([]byte) (int, error) { panic("unreachable") }
|
||||
|
||||
func checkRecover(t *testing.T, name, wantPanic string) {
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("%s: panic = %v; wantPanic %v",
|
||||
name, r, wantPanic)
|
||||
}
|
||||
}
|
||||
14
command/wrap.go
Normal file
14
command/wrap.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package command
|
||||
|
||||
// the top level node wants [Command] returned for its builder methods
|
||||
type rootNode struct{ *node }
|
||||
|
||||
func (r rootNode) Command(name, usage string, f HandlerFunc) Command {
|
||||
r.node.Command(name, usage, f)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r rootNode) Flag(p any, name string, value FlagDefiner, usage string) Command {
|
||||
r.node.Flag(p, name, value, usage)
|
||||
return r
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
#compdef fortify
|
||||
|
||||
_fortify_app() {
|
||||
__fortify_files
|
||||
return $?
|
||||
}
|
||||
|
||||
_fortify_run() {
|
||||
_arguments \
|
||||
'--id[App ID, leave empty to disable security context app_id]:id' \
|
||||
'-a[Fortify application ID]: :_numbers' \
|
||||
'-g[Groups inherited by the app process]: :_groups' \
|
||||
'-d[Application home directory]: :_files -/' \
|
||||
'-u[Passwd name within sandbox]: :_users' \
|
||||
'--wayland[Share Wayland socket]' \
|
||||
'-X[Share X11 socket and allow connection]' \
|
||||
'--dbus[Proxy D-Bus connection]' \
|
||||
'--pulse[Share PulseAudio socket and cookie]' \
|
||||
'--dbus-config[Path to D-Bus proxy config file]: :_files -g "*.json"' \
|
||||
'--dbus-system[Path to system D-Bus proxy config file]: :_files -g "*.json"' \
|
||||
'--mpris[Allow owning MPRIS D-Bus path]' \
|
||||
'--dbus-log[Force logging in the D-Bus proxy]'
|
||||
}
|
||||
|
||||
_fortify_ps() {
|
||||
_arguments \
|
||||
'--short[Print instance id]'
|
||||
}
|
||||
|
||||
_fortify_show() {
|
||||
_alternative \
|
||||
'instances:domains:__fortify_instances' \
|
||||
'files:files:__fortify_files'
|
||||
}
|
||||
|
||||
__fortify_files() {
|
||||
_files -g "*.(json|ftfy)"
|
||||
return $?
|
||||
}
|
||||
|
||||
__fortify_instances() {
|
||||
local -a out
|
||||
shift -p
|
||||
out=( ${(f)"$(_call_program commands fortify ps --short 2>&1)"} )
|
||||
if (( $#out == 0 )); then
|
||||
_message "No active instances"
|
||||
else
|
||||
_describe "active instances" out
|
||||
fi
|
||||
return $?
|
||||
}
|
||||
|
||||
(( $+functions[_fortify_commands] )) || _fortify_commands()
|
||||
{
|
||||
local -a _fortify_cmds
|
||||
_fortify_cmds=(
|
||||
"app:Launch app defined by the specified config file"
|
||||
"run:Configure and start a permissive default sandbox"
|
||||
"show:Show the contents of an app configuration"
|
||||
"ps:List active apps and their state"
|
||||
"version:Show fortify version"
|
||||
"license:Show full license text"
|
||||
"template:Produce a config template"
|
||||
"help:Show help message"
|
||||
)
|
||||
if (( CURRENT == 1 )); then
|
||||
_describe -t commands 'action' _fortify_cmds || compadd "$@"
|
||||
else
|
||||
local curcontext="$curcontext"
|
||||
cmd="${${_fortify_cmds[(r)$words[1]:*]%%:*}}"
|
||||
if (( $+functions[_fortify_$cmd] )); then
|
||||
_fortify_$cmd
|
||||
else
|
||||
_message "no more options"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_arguments -C \
|
||||
'-v[Verbose output]' \
|
||||
'--json[Format output in JSON when applicable]' \
|
||||
'*::fortify command:_fortify_commands'
|
||||
229
container/container.go
Normal file
229
container/container.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// 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"
|
||||
"path"
|
||||
"strconv"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
type (
|
||||
// Container represents a container environment being prepared or run.
|
||||
// None of [Container] methods are safe for concurrent use.
|
||||
Container struct {
|
||||
// Name of initial process in the container.
|
||||
name string
|
||||
// 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
|
||||
|
||||
// Custom [exec.Cmd] initialisation function.
|
||||
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
|
||||
|
||||
// param encoder for shim and init
|
||||
setup *gob.Encoder
|
||||
// cancels cmd
|
||||
cancel context.CancelFunc
|
||||
|
||||
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 string
|
||||
// Initial process environment.
|
||||
Env []string
|
||||
// Absolute path of initial process in the container. Overrides name.
|
||||
Path string
|
||||
// Initial process argv.
|
||||
Args []string
|
||||
|
||||
// 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
|
||||
// Retain CAP_SYS_ADMIN.
|
||||
Privileged bool
|
||||
}
|
||||
)
|
||||
|
||||
func (p *Container) Start() error {
|
||||
if p.cmd != nil {
|
||||
return errors.New("sandbox: already started")
|
||||
}
|
||||
if p.Ops == nil || len(*p.Ops) == 0 {
|
||||
return errors.New("sandbox: starting an empty container")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(p.ctx)
|
||||
p.cancel = cancel
|
||||
|
||||
var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP
|
||||
if !p.HostNet {
|
||||
cloneFlags |= CLONE_NEWNET
|
||||
}
|
||||
|
||||
// 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.CommandContext != nil {
|
||||
p.cmd = p.CommandContext(ctx)
|
||||
} else {
|
||||
p.cmd = exec.CommandContext(ctx, MustExecutable())
|
||||
p.cmd.Args = []string{"init"}
|
||||
}
|
||||
|
||||
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
|
||||
p.cmd.WaitDelay = p.WaitDelay
|
||||
if p.Cancel != nil {
|
||||
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
||||
} else {
|
||||
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) }
|
||||
}
|
||||
p.cmd.Dir = "/"
|
||||
p.cmd.SysProcAttr = &SysProcAttr{
|
||||
Setsid: !p.RetainSession,
|
||||
Pdeathsig: SIGKILL,
|
||||
Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS,
|
||||
|
||||
// remain privileged for setup
|
||||
AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
|
||||
|
||||
UseCgroupFD: p.Cgroup != nil,
|
||||
}
|
||||
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||
}
|
||||
|
||||
// place setup pipe before user supplied extra files, this is later restored by init
|
||||
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot create shim setup pipe:")
|
||||
} else {
|
||||
p.setup = e
|
||||
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||
}
|
||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||
|
||||
msg.Verbose("starting container init")
|
||||
if err := p.cmd.Start(); err != nil {
|
||||
return msg.WrapErr(err, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Container) Serve() error {
|
||||
if p.setup == nil {
|
||||
panic("invalid serve")
|
||||
}
|
||||
|
||||
setup := p.setup
|
||||
p.setup = nil
|
||||
|
||||
if p.Path != "" && !path.IsAbs(p.Path) {
|
||||
p.cancel()
|
||||
return msg.WrapErr(EINVAL,
|
||||
fmt.Sprintf("invalid executable path %q", p.Path))
|
||||
}
|
||||
|
||||
if p.Path == "" {
|
||||
if p.name == "" {
|
||||
p.Path = os.Getenv("SHELL")
|
||||
if !path.IsAbs(p.Path) {
|
||||
p.cancel()
|
||||
return msg.WrapErr(EBADE,
|
||||
"no command specified and $SHELL is invalid")
|
||||
}
|
||||
p.name = path.Base(p.Path)
|
||||
} else if path.IsAbs(p.name) {
|
||||
p.Path = p.name
|
||||
} else if v, err := exec.LookPath(p.name); err != nil {
|
||||
p.cancel()
|
||||
return msg.WrapErr(err, err.Error())
|
||||
} else {
|
||||
p.Path = v
|
||||
}
|
||||
}
|
||||
|
||||
if p.SeccompRules == nil {
|
||||
// do not transmit 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
|
||||
}
|
||||
|
||||
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func New(ctx context.Context, name string, args ...string) *Container {
|
||||
return &Container{name: name, ctx: ctx,
|
||||
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
|
||||
}
|
||||
}
|
||||
281
container/container_test.go
Normal file
281
container/container_test.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/vfs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/ldd"
|
||||
)
|
||||
|
||||
const (
|
||||
ignore = "\x00"
|
||||
ignoreV = -1
|
||||
)
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
{
|
||||
oldVerbose := hlog.Load()
|
||||
oldOutput := container.GetOutput()
|
||||
internal.InstallOutput(true)
|
||||
t.Cleanup(func() { hlog.Store(oldVerbose) })
|
||||
t.Cleanup(func() { container.SetOutput(oldOutput) })
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
filter bool
|
||||
session bool
|
||||
net bool
|
||||
ops *container.Ops
|
||||
mnt []*vfs.MountInfoEntry
|
||||
host string
|
||||
rules []seccomp.NativeRule
|
||||
flags seccomp.ExportFlag
|
||||
presets seccomp.FilterPreset
|
||||
}{
|
||||
{"minimal", true, false, false,
|
||||
new(container.Ops), nil, "test-minimal",
|
||||
nil, 0, seccomp.PresetStrict},
|
||||
{"allow", true, true, true,
|
||||
new(container.Ops), nil, "test-minimal",
|
||||
nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
|
||||
{"no filter", false, true, true,
|
||||
new(container.Ops), nil, "test-no-filter",
|
||||
nil, 0, seccomp.PresetExt},
|
||||
{"custom rules", true, true, true,
|
||||
new(container.Ops), nil, "test-no-filter",
|
||||
[]seccomp.NativeRule{
|
||||
{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil},
|
||||
}, 0, seccomp.PresetExt},
|
||||
{"tmpfs", true, false, false,
|
||||
new(container.Ops).
|
||||
Tmpfs(hst.Tmp, 0, 0755),
|
||||
[]*vfs.MountInfoEntry{
|
||||
e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||
}, "test-tmpfs",
|
||||
nil, 0, seccomp.PresetStrict},
|
||||
{"dev", true, true /* go test output is not a tty */, false,
|
||||
new(container.Ops).
|
||||
Dev("/dev").
|
||||
Mqueue("/dev/mqueue"),
|
||||
[]*vfs.MountInfoEntry{
|
||||
e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||
e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
|
||||
}, "",
|
||||
nil, 0, seccomp.PresetStrict},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v",
|
||||
"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
|
||||
c.Uid = 1000
|
||||
c.Gid = 100
|
||||
c.Hostname = tc.host
|
||||
c.CommandContext = commandContext
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
c.Ops = tc.ops
|
||||
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
|
||||
if c.Args[5] == "" {
|
||||
if name, err := os.Hostname(); err != nil {
|
||||
t.Fatalf("cannot get hostname: %v", err)
|
||||
} else {
|
||||
c.Args[5] = name
|
||||
}
|
||||
}
|
||||
|
||||
c.
|
||||
Tmpfs("/tmp", 0, 0755).
|
||||
Bind(os.Args[0], os.Args[0], 0).
|
||||
Mkdir("/usr/bin", 0755).
|
||||
Link(os.Args[0], "/usr/bin/sandbox.test").
|
||||
Place("/etc/hostname", []byte(c.Args[5]))
|
||||
// in case test has cgo enabled
|
||||
var libPaths []string
|
||||
if entries, err := ldd.ExecFilter(ctx,
|
||||
commandContext,
|
||||
func(v []byte) []byte {
|
||||
return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
|
||||
}, os.Args[0]); err != nil {
|
||||
log.Fatalf("ldd: %v", err)
|
||||
} else {
|
||||
libPaths = ldd.Path(entries)
|
||||
}
|
||||
for _, name := range libPaths {
|
||||
c.Bind(name, name, 0)
|
||||
}
|
||||
// needs /proc to check mountinfo
|
||||
c.Proc("/proc")
|
||||
|
||||
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
|
||||
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
|
||||
mnt = append(mnt, tc.mnt...)
|
||||
mnt = append(mnt,
|
||||
e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||
e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
|
||||
e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
|
||||
)
|
||||
for _, name := range libPaths {
|
||||
mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
|
||||
}
|
||||
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
|
||||
want := new(bytes.Buffer)
|
||||
if err := gob.NewEncoder(want).Encode(mnt); err != nil {
|
||||
t.Fatalf("cannot serialise expected mount points: %v", err)
|
||||
}
|
||||
c.Stdin = want
|
||||
|
||||
if err := c.Start(); err != nil {
|
||||
hlog.PrintBaseError(err, "start:")
|
||||
t.Fatalf("cannot start container: %v", err)
|
||||
} else if err = c.Serve(); err != nil {
|
||||
hlog.PrintBaseError(err, "serve:")
|
||||
t.Errorf("cannot serve setup params: %v", err)
|
||||
}
|
||||
if err := c.Wait(); err != nil {
|
||||
hlog.PrintBaseError(err, "wait:")
|
||||
t.Fatalf("wait: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func e(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 TestContainerString(t *testing.T) {
|
||||
c := container.New(t.Context(), "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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperInit(t *testing.T) {
|
||||
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||
return
|
||||
}
|
||||
container.SetOutput(hlog.Output{})
|
||||
container.Init(hlog.Prepare, internal.InstallOutput)
|
||||
}
|
||||
|
||||
func TestHelperCheckContainer(t *testing.T) {
|
||||
if len(os.Args) != 6 || os.Args[4] != "check" {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("user", func(t *testing.T) {
|
||||
if uid := syscall.Getuid(); uid != 1000 {
|
||||
t.Errorf("Getuid: %d, want 1000", uid)
|
||||
}
|
||||
if gid := syscall.Getgid(); gid != 100 {
|
||||
t.Errorf("Getgid: %d, want 100", gid)
|
||||
}
|
||||
})
|
||||
t.Run("hostname", func(t *testing.T) {
|
||||
if name, err := os.Hostname(); err != nil {
|
||||
t.Fatalf("cannot get hostname: %v", err)
|
||||
} else if name != os.Args[5] {
|
||||
t.Errorf("Hostname: %q, want %q", name, os.Args[5])
|
||||
}
|
||||
|
||||
if p, err := os.ReadFile("/etc/hostname"); err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
} else if string(p) != os.Args[5] {
|
||||
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
|
||||
}
|
||||
})
|
||||
t.Run("mount", func(t *testing.T) {
|
||||
var mnt []*vfs.MountInfoEntry
|
||||
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
|
||||
t.Fatalf("cannot receive expected mount points: %v", err)
|
||||
}
|
||||
|
||||
var d *vfs.MountInfoDecoder
|
||||
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
|
||||
t.Fatalf("cannot open mountinfo: %v", err)
|
||||
} else {
|
||||
d = vfs.NewMountInfoDecoder(f)
|
||||
}
|
||||
|
||||
i := 0
|
||||
for cur := range d.Entries() {
|
||||
if i == len(mnt) {
|
||||
t.Errorf("got more than %d entries", len(mnt))
|
||||
break
|
||||
}
|
||||
|
||||
// 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") {
|
||||
t.Errorf("[FAIL] %s", cur)
|
||||
} else {
|
||||
t.Logf("[ OK ] %s", cur)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
if err := d.Err(); err != nil {
|
||||
t.Errorf("cannot parse mountinfo: %v", err)
|
||||
}
|
||||
|
||||
if i != len(mnt) {
|
||||
t.Errorf("got %d entries, want %d", i, len(mnt))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func commandContext(ctx context.Context) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, os.Args[0], "-test.v",
|
||||
"-test.run=TestHelperInit", "--", "init")
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package internal
|
||||
package container
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -15,7 +13,7 @@ var (
|
||||
|
||||
func copyExecutable() {
|
||||
if name, err := os.Executable(); err != nil {
|
||||
fmsg.BeforeExit()
|
||||
msg.BeforeExit()
|
||||
log.Fatalf("cannot read executable path: %v", err)
|
||||
} else {
|
||||
executable = name
|
||||
17
container/executable_test.go
Normal file
17
container/executable_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
func TestExecutable(t *testing.T) {
|
||||
for i := 0; i < 16; i++ {
|
||||
if got := container.MustExecutable(); got != os.Args[0] {
|
||||
t.Errorf("MustExecutable: %q, want %q",
|
||||
got, os.Args[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
364
container/init.go
Normal file
364
container/init.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
const (
|
||||
// time to wait for linger processes after death of initial process
|
||||
residualProcessTimeout = 5 * time.Second
|
||||
|
||||
// intermediate tmpfs mount point
|
||||
basePath = "/tmp"
|
||||
|
||||
// setup params file descriptor
|
||||
setupEnv = "HAKUREI_SETUP"
|
||||
)
|
||||
|
||||
type initParams struct {
|
||||
Params
|
||||
|
||||
HostUid, HostGid int
|
||||
// extra files count
|
||||
Count int
|
||||
// verbosity pass through
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
runtime.LockOSThread()
|
||||
prepare("init")
|
||||
|
||||
if os.Getpid() != 1 {
|
||||
log.Fatal("this process must run as pid 1")
|
||||
}
|
||||
|
||||
var (
|
||||
params initParams
|
||||
closeSetup func() error
|
||||
setupFile *os.File
|
||||
offsetSetup int
|
||||
)
|
||||
if f, err := Receive(setupEnv, ¶ms, &setupFile); err != nil {
|
||||
if errors.Is(err, ErrInvalid) {
|
||||
log.Fatal("invalid setup descriptor")
|
||||
}
|
||||
if errors.Is(err, ErrNotSet) {
|
||||
log.Fatal("HAKUREI_SETUP not set")
|
||||
}
|
||||
|
||||
log.Fatalf("cannot decode init setup payload: %v", err)
|
||||
} else {
|
||||
if params.Ops == nil {
|
||||
log.Fatal("invalid setup parameters")
|
||||
}
|
||||
if params.ParentPerm == 0 {
|
||||
params.ParentPerm = 0755
|
||||
}
|
||||
|
||||
setVerbose(params.Verbose)
|
||||
msg.Verbose("received setup parameters")
|
||||
closeSetup = f
|
||||
offsetSetup = int(setupFile.Fd() + 1)
|
||||
}
|
||||
|
||||
// write uid/gid map here so parent does not need to set dumpable
|
||||
if err := SetDumpable(SUID_DUMP_USER); err != nil {
|
||||
log.Fatalf("cannot set SUID_DUMP_USER: %s", err)
|
||||
}
|
||||
if err := os.WriteFile("/proc/self/uid_map",
|
||||
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
|
||||
0); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
if err := os.WriteFile("/proc/self/setgroups",
|
||||
[]byte("deny\n"),
|
||||
0); err != nil && !os.IsNotExist(err) {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
if err := os.WriteFile("/proc/self/gid_map",
|
||||
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
|
||||
0); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
if err := SetDumpable(SUID_DUMP_DISABLE); err != nil {
|
||||
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
}
|
||||
|
||||
oldmask := Umask(0)
|
||||
if params.Hostname != "" {
|
||||
if err := Sethostname([]byte(params.Hostname)); err != nil {
|
||||
log.Fatalf("cannot set hostname: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// cache sysctl before pivot_root
|
||||
LastCap()
|
||||
|
||||
if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil {
|
||||
log.Fatalf("cannot make / rslave: %v", err)
|
||||
}
|
||||
|
||||
for i, op := range *params.Ops {
|
||||
if op == nil {
|
||||
log.Fatalf("invalid op %d", i)
|
||||
}
|
||||
|
||||
if err := op.early(¶ms.Params); err != nil {
|
||||
msg.PrintBaseErr(err,
|
||||
fmt.Sprintf("cannot prepare op %d:", i))
|
||||
msg.BeforeExit()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := Mount("rootfs", basePath, "tmpfs", MS_NODEV|MS_NOSUID, ""); err != nil {
|
||||
log.Fatalf("cannot mount intermediate root: %v", err)
|
||||
}
|
||||
if err := os.Chdir(basePath); err != nil {
|
||||
log.Fatalf("cannot enter base path: %v", err)
|
||||
}
|
||||
|
||||
if err := os.Mkdir(sysrootDir, 0755); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil {
|
||||
log.Fatalf("cannot bind sysroot: %v", err)
|
||||
}
|
||||
|
||||
if err := os.Mkdir(hostDir, 0755); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
// pivot_root uncovers basePath in hostDir
|
||||
if err := PivotRoot(basePath, hostDir); err != nil {
|
||||
log.Fatalf("cannot pivot into intermediate root: %v", err)
|
||||
}
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
for i, op := range *params.Ops {
|
||||
// ops already checked during early setup
|
||||
msg.Verbosef("%s %s", op.prefix(), op)
|
||||
if err := op.apply(¶ms.Params); err != nil {
|
||||
msg.PrintBaseErr(err,
|
||||
fmt.Sprintf("cannot apply op %d:", i))
|
||||
msg.BeforeExit()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// setup requiring host root complete at this point
|
||||
if err := Mount(hostDir, hostDir, "", MS_SILENT|MS_REC|MS_PRIVATE, ""); err != nil {
|
||||
log.Fatalf("cannot make host root rprivate: %v", err)
|
||||
}
|
||||
if err := Unmount(hostDir, MNT_DETACH); err != nil {
|
||||
log.Fatalf("cannot unmount host root: %v", err)
|
||||
}
|
||||
|
||||
{
|
||||
var fd int
|
||||
if err := IgnoringEINTR(func() (err error) {
|
||||
fd, err = Open("/", O_DIRECTORY|O_RDONLY, 0)
|
||||
return
|
||||
}); err != nil {
|
||||
log.Fatalf("cannot open intermediate root: %v", err)
|
||||
}
|
||||
if err := os.Chdir(sysrootPath); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
if err := PivotRoot(".", "."); err != nil {
|
||||
log.Fatalf("cannot pivot into sysroot: %v", err)
|
||||
}
|
||||
if err := Fchdir(fd); err != nil {
|
||||
log.Fatalf("cannot re-enter intermediate root: %v", err)
|
||||
}
|
||||
if err := Unmount(".", MNT_DETACH); err != nil {
|
||||
log.Fatalf("cannot unmount intemediate root: %v", err)
|
||||
}
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
if err := Close(fd); err != nil {
|
||||
log.Fatalf("cannot close intermediate root: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, _, errno := Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
|
||||
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
|
||||
}
|
||||
|
||||
if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
|
||||
log.Fatalf("cannot clear the ambient capability set: %v", errno)
|
||||
}
|
||||
for i := uintptr(0); i <= LastCap(); i++ {
|
||||
if params.Privileged && i == CAP_SYS_ADMIN {
|
||||
continue
|
||||
}
|
||||
if _, _, errno := Syscall(SYS_PRCTL, PR_CAPBSET_DROP, i, 0); errno != 0 {
|
||||
log.Fatalf("cannot drop capability from bonding set: %v", errno)
|
||||
}
|
||||
}
|
||||
|
||||
var keep [2]uint32
|
||||
if params.Privileged {
|
||||
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
||||
|
||||
if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 {
|
||||
log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno)
|
||||
}
|
||||
}
|
||||
if err := capset(
|
||||
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
|
||||
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
|
||||
); err != nil {
|
||||
log.Fatalf("cannot capset: %v", err)
|
||||
}
|
||||
|
||||
if !params.SeccompDisable {
|
||||
rules := params.SeccompRules
|
||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
|
||||
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
|
||||
}
|
||||
if err := seccomp.Load(rules, params.SeccompFlags); err != nil {
|
||||
log.Fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
msg.Verbosef("%d filter rules loaded", len(rules))
|
||||
} else {
|
||||
msg.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] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
||||
}
|
||||
Umask(oldmask)
|
||||
|
||||
cmd := exec.Command(params.Path)
|
||||
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
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
msg.Suspend()
|
||||
|
||||
if err := closeSetup(); err != nil {
|
||||
log.Println("cannot close setup pipe:", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
type winfo struct {
|
||||
wpid int
|
||||
wstatus WaitStatus
|
||||
}
|
||||
info := make(chan winfo, 1)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
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 = Wait4(-1, &wstatus, 0, nil)
|
||||
}
|
||||
}
|
||||
if !errors.Is(err, ECHILD) {
|
||||
log.Println("unexpected wait4 response:", err)
|
||||
}
|
||||
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// handle signals to dump withheld messages
|
||||
sig := make(chan os.Signal, 2)
|
||||
signal.Notify(sig, SIGINT, SIGTERM)
|
||||
|
||||
// closed after residualProcessTimeout has elapsed after initial process death
|
||||
timeout := make(chan struct{})
|
||||
|
||||
r := 2
|
||||
for {
|
||||
select {
|
||||
case s := <-sig:
|
||||
if msg.Resume() {
|
||||
msg.Verbosef("terminating on %s after process start", s.String())
|
||||
} else {
|
||||
msg.Verbosef("terminating on %s", s.String())
|
||||
}
|
||||
os.Exit(0)
|
||||
case w := <-info:
|
||||
if w.wpid == cmd.Process.Pid {
|
||||
// initial process exited, output is most likely available again
|
||||
msg.Resume()
|
||||
|
||||
switch {
|
||||
case w.wstatus.Exited():
|
||||
r = w.wstatus.ExitStatus()
|
||||
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
|
||||
case w.wstatus.Signaled():
|
||||
r = 128 + int(w.wstatus.Signal())
|
||||
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
|
||||
default:
|
||||
r = 255
|
||||
msg.Verbosef("initial process exited with status %#x", w.wstatus)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(residualProcessTimeout)
|
||||
close(timeout)
|
||||
}()
|
||||
}
|
||||
case <-done:
|
||||
msg.BeforeExit()
|
||||
os.Exit(r)
|
||||
case <-timeout:
|
||||
log.Println("timeout exceeded waiting for lingering processes")
|
||||
msg.BeforeExit()
|
||||
os.Exit(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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]) == "init" {
|
||||
msg = v
|
||||
Init(prepare, setVerbose)
|
||||
msg.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
123
container/mount.go
Normal file
123
container/mount.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
. "syscall"
|
||||
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
if eq {
|
||||
msg.Verbosef("resolved %q flags %#x", target, flags)
|
||||
} else {
|
||||
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
|
||||
}
|
||||
|
||||
if err := Mount(source, target, "", MS_SILENT|MS_BIND|flags&MS_REC, ""); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot mount %q on %q:", source, target))
|
||||
}
|
||||
|
||||
var targetFinal string
|
||||
if v, err := filepath.EvalSymlinks(target); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
targetFinal = v
|
||||
if targetFinal != target {
|
||||
msg.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 = Open(targetFinal, O_PATH|O_CLOEXEC, 0)
|
||||
return
|
||||
}); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot open %q:", targetFinal))
|
||||
}
|
||||
if v, err := os.Readlink(p.fd(destFd)); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if err = Close(destFd); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot close %q:", targetFinal))
|
||||
} else {
|
||||
targetKFinal = v
|
||||
}
|
||||
}
|
||||
|
||||
mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY
|
||||
return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
|
||||
n, err := d.Unfold(targetKFinal)
|
||||
if err != nil {
|
||||
if errors.Is(err, ESTALE) {
|
||||
return msg.WrapErr(err,
|
||||
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
|
||||
}
|
||||
return wrapErrSuffix(err,
|
||||
"cannot unfold mount hierarchy:")
|
||||
}
|
||||
|
||||
if err = remountWithFlags(n, mf); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags&MS_REC == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for cur := range n.Collective() {
|
||||
err = remountWithFlags(cur, mf)
|
||||
if err != nil && !errors.Is(err, EACCES) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
|
||||
kf, unmatched := n.Flags()
|
||||
if len(unmatched) != 0 {
|
||||
msg.Verbosef("unmatched vfs options: %q", unmatched)
|
||||
}
|
||||
|
||||
if kf&mf != mf {
|
||||
return wrapErrSuffix(
|
||||
Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""),
|
||||
fmt.Sprintf("cannot remount %q:", n.Clean))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
|
||||
target := toSysroot(name)
|
||||
if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
opt := fmt.Sprintf("mode=%#o", perm)
|
||||
if size > 0 {
|
||||
opt += fmt.Sprintf(",size=%d", size)
|
||||
}
|
||||
return wrapErrSuffix(
|
||||
Mount(fsname, target, "tmpfs", MS_NOSUID|MS_NODEV, opt),
|
||||
fmt.Sprintf("cannot mount tmpfs on %q:", name))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
43
container/msg.go
Normal file
43
container/msg.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Msg interface {
|
||||
IsVerbose() bool
|
||||
Verbose(v ...any)
|
||||
Verbosef(format string, v ...any)
|
||||
WrapErr(err error, a ...any) error
|
||||
PrintBaseErr(err error, fallback string)
|
||||
|
||||
Suspend()
|
||||
Resume() bool
|
||||
|
||||
BeforeExit()
|
||||
}
|
||||
|
||||
type DefaultMsg struct{ inactive atomic.Bool }
|
||||
|
||||
func (msg *DefaultMsg) IsVerbose() bool { return true }
|
||||
func (msg *DefaultMsg) Verbose(v ...any) {
|
||||
if !msg.inactive.Load() {
|
||||
log.Println(v...)
|
||||
}
|
||||
}
|
||||
func (msg *DefaultMsg) Verbosef(format string, v ...any) {
|
||||
if !msg.inactive.Load() {
|
||||
log.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
|
||||
log.Println(a...)
|
||||
return err
|
||||
}
|
||||
func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
|
||||
|
||||
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
|
||||
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
|
||||
func (msg *DefaultMsg) BeforeExit() {}
|
||||
482
container/ops.go
Normal file
482
container/ops.go
Normal file
@@ -0,0 +1,482 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
. "syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type (
|
||||
Ops []Op
|
||||
Op interface {
|
||||
// early is called in host root.
|
||||
early(params *Params) error
|
||||
// apply is called in intermediate root.
|
||||
apply(params *Params) error
|
||||
|
||||
prefix() string
|
||||
Is(op Op) bool
|
||||
fmt.Stringer
|
||||
}
|
||||
)
|
||||
|
||||
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
||||
|
||||
func init() { gob.Register(new(BindMountOp)) }
|
||||
|
||||
// BindMountOp bind mounts host path Source on container path Target.
|
||||
type BindMountOp struct {
|
||||
Source, SourceFinal, Target string
|
||||
|
||||
Flags int
|
||||
}
|
||||
|
||||
const (
|
||||
BindOptional = 1 << iota
|
||||
BindWritable
|
||||
BindDevice
|
||||
)
|
||||
|
||||
func (b *BindMountOp) early(*Params) error {
|
||||
if !path.IsAbs(b.Source) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source))
|
||||
}
|
||||
|
||||
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
|
||||
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
|
||||
b.SourceFinal = "\x00"
|
||||
return nil
|
||||
}
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
b.SourceFinal = v
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BindMountOp) apply(*Params) error {
|
||||
if b.SourceFinal == "\x00" {
|
||||
if b.Flags&BindOptional == 0 {
|
||||
// unreachable
|
||||
return EBADE
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
|
||||
return msg.WrapErr(EBADE, "path is not absolute")
|
||||
}
|
||||
|
||||
source := toHost(b.SourceFinal)
|
||||
target := toSysroot(b.Target)
|
||||
|
||||
// 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 := os.Stat(source); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if fi.IsDir() {
|
||||
if err = os.MkdirAll(target, 0700); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
} else if err = ensureFile(target, 0444, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var flags uintptr = MS_REC
|
||||
if b.Flags&BindWritable == 0 {
|
||||
flags |= MS_RDONLY
|
||||
}
|
||||
if b.Flags&BindDevice == 0 {
|
||||
flags |= MS_NODEV
|
||||
}
|
||||
|
||||
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
|
||||
}
|
||||
|
||||
func (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb }
|
||||
func (*BindMountOp) prefix() string { return "mounting" }
|
||||
func (b *BindMountOp) String() string {
|
||||
if b.Source == b.Target {
|
||||
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
|
||||
}
|
||||
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
|
||||
}
|
||||
func (f *Ops) Bind(source, target string, flags int) *Ops {
|
||||
*f = append(*f, &BindMountOp{source, "", target, flags})
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MountProcOp)) }
|
||||
|
||||
// MountProcOp mounts a private instance of proc.
|
||||
type MountProcOp string
|
||||
|
||||
func (p MountProcOp) early(*Params) error { return nil }
|
||||
func (p MountProcOp) apply(params *Params) error {
|
||||
v := string(p)
|
||||
|
||||
if !path.IsAbs(v) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
|
||||
}
|
||||
|
||||
target := toSysroot(v)
|
||||
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return wrapErrSuffix(Mount("proc", target, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
|
||||
fmt.Sprintf("cannot mount proc on %q:", v))
|
||||
}
|
||||
|
||||
func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp }
|
||||
func (MountProcOp) prefix() string { return "mounting" }
|
||||
func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) }
|
||||
func (f *Ops) Proc(dest string) *Ops {
|
||||
*f = append(*f, MountProcOp(dest))
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MountDevOp)) }
|
||||
|
||||
// MountDevOp mounts part of host dev.
|
||||
type MountDevOp string
|
||||
|
||||
func (d MountDevOp) early(*Params) error { return nil }
|
||||
func (d MountDevOp) apply(params *Params) error {
|
||||
v := string(d)
|
||||
|
||||
if !path.IsAbs(v) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
|
||||
}
|
||||
target := toSysroot(v)
|
||||
|
||||
if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
|
||||
targetPath := toSysroot(path.Join(v, name))
|
||||
if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hostProc.bindMount(
|
||||
toHost("/dev/"+name),
|
||||
targetPath,
|
||||
0,
|
||||
true,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i, name := range []string{"stdin", "stdout", "stderr"} {
|
||||
if err := os.Symlink(
|
||||
"/proc/self/fd/"+string(rune(i+'0')),
|
||||
path.Join(target, name),
|
||||
); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
for _, pair := range [][2]string{
|
||||
{"/proc/self/fd", "fd"},
|
||||
{"/proc/kcore", "core"},
|
||||
{"pts/ptmx", "ptmx"},
|
||||
} {
|
||||
if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
|
||||
devPtsPath := path.Join(target, "pts")
|
||||
for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
|
||||
if err := os.Mkdir(name, params.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := Mount("devpts", devPtsPath, "devpts", MS_NOSUID|MS_NOEXEC,
|
||||
"newinstance,ptmxmode=0666,mode=620"); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
|
||||
}
|
||||
|
||||
if params.RetainSession {
|
||||
var buf [8]byte
|
||||
if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 {
|
||||
consolePath := toSysroot(path.Join(v, "console"))
|
||||
if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if name, err := os.Readlink(hostProc.stdout()); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if err = hostProc.bindMount(
|
||||
toHost(name),
|
||||
consolePath,
|
||||
0,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd }
|
||||
func (MountDevOp) prefix() string { return "mounting" }
|
||||
func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) }
|
||||
func (f *Ops) Dev(dest string) *Ops {
|
||||
*f = append(*f, MountDevOp(dest))
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MountMqueueOp)) }
|
||||
|
||||
// MountMqueueOp mounts a private mqueue instance on container Path.
|
||||
type MountMqueueOp string
|
||||
|
||||
func (m MountMqueueOp) early(*Params) error { return nil }
|
||||
func (m MountMqueueOp) apply(params *Params) error {
|
||||
v := string(m)
|
||||
|
||||
if !path.IsAbs(v) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
|
||||
}
|
||||
|
||||
target := toSysroot(v)
|
||||
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
|
||||
fmt.Sprintf("cannot mount mqueue on %q:", v))
|
||||
}
|
||||
|
||||
func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm }
|
||||
func (MountMqueueOp) prefix() string { return "mounting" }
|
||||
func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
|
||||
func (f *Ops) Mqueue(dest string) *Ops {
|
||||
*f = append(*f, MountMqueueOp(dest))
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MountTmpfsOp)) }
|
||||
|
||||
// MountTmpfsOp mounts tmpfs on container Path.
|
||||
type MountTmpfsOp struct {
|
||||
Path string
|
||||
Size int
|
||||
Perm os.FileMode
|
||||
}
|
||||
|
||||
func (t *MountTmpfsOp) early(*Params) error { return nil }
|
||||
func (t *MountTmpfsOp) apply(*Params) error {
|
||||
if !path.IsAbs(t.Path) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
|
||||
}
|
||||
if t.Size < 0 || t.Size > math.MaxUint>>1 {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size))
|
||||
}
|
||||
return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
|
||||
}
|
||||
|
||||
func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt }
|
||||
func (*MountTmpfsOp) prefix() string { return "mounting" }
|
||||
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
|
||||
*f = append(*f, &MountTmpfsOp{dest, size, perm})
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(SymlinkOp)) }
|
||||
|
||||
// SymlinkOp creates a symlink in the container filesystem.
|
||||
type SymlinkOp [2]string
|
||||
|
||||
func (l *SymlinkOp) early(*Params) error {
|
||||
if strings.HasPrefix(l[0], "*") {
|
||||
l[0] = l[0][1:]
|
||||
if !path.IsAbs(l[0]) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0]))
|
||||
}
|
||||
if name, err := os.Readlink(l[0]); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
l[0] = name
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (l *SymlinkOp) apply(params *Params) error {
|
||||
// symlink target is an arbitrary path value, so only validate link name here
|
||||
if !path.IsAbs(l[1]) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1]))
|
||||
}
|
||||
|
||||
target := toSysroot(l[1])
|
||||
if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
if err := os.Symlink(l[0], target); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl }
|
||||
func (*SymlinkOp) prefix() string { return "creating" }
|
||||
func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
|
||||
func (f *Ops) Link(target, linkName string) *Ops {
|
||||
*f = append(*f, &SymlinkOp{target, linkName})
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MkdirOp)) }
|
||||
|
||||
// MkdirOp creates a directory in the container filesystem.
|
||||
type MkdirOp struct {
|
||||
Path string
|
||||
Perm os.FileMode
|
||||
}
|
||||
|
||||
func (m *MkdirOp) early(*Params) error { return nil }
|
||||
func (m *MkdirOp) apply(*Params) error {
|
||||
if !path.IsAbs(m.Path) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path))
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm }
|
||||
func (*MkdirOp) prefix() string { return "creating" }
|
||||
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
||||
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
|
||||
*f = append(*f, &MkdirOp{dest, perm})
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(TmpfileOp)) }
|
||||
|
||||
// TmpfileOp places a file in container Path containing Data.
|
||||
type TmpfileOp struct {
|
||||
Path string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (t *TmpfileOp) early(*Params) error { return nil }
|
||||
func (t *TmpfileOp) apply(params *Params) error {
|
||||
if !path.IsAbs(t.Path) {
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
|
||||
}
|
||||
|
||||
var tmpPath string
|
||||
if f, err := os.CreateTemp("/", "tmp.*"); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if _, err = f.Write(t.Data); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot write to intermediate file:")
|
||||
} else if err = f.Close(); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot close intermediate file:")
|
||||
} else {
|
||||
tmpPath = f.Name()
|
||||
}
|
||||
|
||||
target := toSysroot(t.Path)
|
||||
if err := ensureFile(target, 0444, params.ParentPerm); err != nil {
|
||||
return err
|
||||
} else if err = hostProc.bindMount(
|
||||
tmpPath,
|
||||
target,
|
||||
MS_RDONLY|MS_NODEV,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
} else if err = os.Remove(tmpPath); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TmpfileOp) Is(op Op) bool {
|
||||
vt, ok := op.(*TmpfileOp)
|
||||
return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data)
|
||||
}
|
||||
func (*TmpfileOp) prefix() string { return "placing" }
|
||||
func (t *TmpfileOp) String() string {
|
||||
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
|
||||
}
|
||||
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f }
|
||||
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
|
||||
t := &TmpfileOp{Path: name}
|
||||
*dataP = &t.Data
|
||||
|
||||
*f = append(*f, t)
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(AutoEtcOp)) }
|
||||
|
||||
// AutoEtcOp 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.
|
||||
type AutoEtcOp struct{ Prefix string }
|
||||
|
||||
func (e *AutoEtcOp) early(*Params) error { return nil }
|
||||
func (e *AutoEtcOp) apply(*Params) error {
|
||||
const target = sysrootPath + "/etc/"
|
||||
rel := e.hostRel() + "/"
|
||||
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
for _, ent := range d {
|
||||
n := ent.Name()
|
||||
switch n {
|
||||
case ".host":
|
||||
|
||||
case "passwd":
|
||||
case "group":
|
||||
|
||||
case "mtab":
|
||||
if err = os.Symlink("/proc/mounts", target+n); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
|
||||
default:
|
||||
if err = os.Symlink(rel+n, target+n); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (e *AutoEtcOp) hostPath() string { return "/etc/" + 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 == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
|
||||
}
|
||||
func (*AutoEtcOp) prefix() string { return "setting up" }
|
||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||
func (f *Ops) Etc(host, prefix string) *Ops {
|
||||
e := &AutoEtcOp{prefix}
|
||||
f.Mkdir("/etc", 0755)
|
||||
f.Bind(host, e.hostPath(), 0)
|
||||
*f = append(*f, e)
|
||||
return f
|
||||
}
|
||||
26
container/output.go
Normal file
26
container/output.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package container
|
||||
|
||||
var msg Msg = new(DefaultMsg)
|
||||
|
||||
func GetOutput() Msg { return msg }
|
||||
func SetOutput(v Msg) {
|
||||
if v == nil {
|
||||
msg = new(DefaultMsg)
|
||||
} else {
|
||||
msg = v
|
||||
}
|
||||
}
|
||||
|
||||
func wrapErrSuffix(err error, a ...any) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return msg.WrapErr(err, append(a, err)...)
|
||||
}
|
||||
|
||||
func wrapErrSelf(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return msg.WrapErr(err, err.Error())
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package proc
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
@@ -12,7 +12,7 @@ var (
|
||||
ErrInvalid = errors.New("bad file descriptor")
|
||||
)
|
||||
|
||||
// Setup appends the read end of a pipe for payload transmission and returns its fd.
|
||||
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
||||
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return -1, nil, err
|
||||
@@ -23,9 +23,8 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Receive retrieves payload pipe fd from the environment,
|
||||
// receives its payload and returns the Close method of the pipe.
|
||||
func Receive(key string, e any) (func() error, error) {
|
||||
// Receive retrieves setup fd from the environment and receives params.
|
||||
func Receive(key string, e any, v **os.File) (func() error, error) {
|
||||
var setup *os.File
|
||||
|
||||
if s, ok := os.LookupEnv(key); !ok {
|
||||
@@ -38,8 +37,11 @@ func Receive(key string, e any) (func() error, error) {
|
||||
if setup == nil {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
if v != nil {
|
||||
*v = setup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func() error { return setup.Close() }, gob.NewDecoder(setup).Decode(e)
|
||||
return setup.Close, gob.NewDecoder(setup).Decode(e)
|
||||
}
|
||||
94
container/path.go
Normal file
94
container/path.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
const (
|
||||
hostPath = "/" + hostDir
|
||||
hostDir = "host"
|
||||
sysrootPath = "/" + sysrootDir
|
||||
sysrootDir = "sysroot"
|
||||
)
|
||||
|
||||
func toSysroot(name string) string {
|
||||
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
|
||||
return path.Join(sysrootPath, name)
|
||||
}
|
||||
|
||||
func toHost(name string) string {
|
||||
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
|
||||
return path.Join(hostPath, name)
|
||||
}
|
||||
|
||||
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
|
||||
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
|
||||
if err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
if content != nil {
|
||||
_, err = f.Write(content)
|
||||
if err != nil {
|
||||
err = wrapErrSelf(err)
|
||||
}
|
||||
}
|
||||
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 = msg.WrapErr(syscall.EISDIR,
|
||||
fmt.Sprintf("path %q is a directory", name))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var hostProc = newProcPats(hostPath)
|
||||
|
||||
func newProcPats(prefix string) *procPaths {
|
||||
return &procPaths{prefix + "/proc", prefix + "/proc/self"}
|
||||
}
|
||||
|
||||
type procPaths struct {
|
||||
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 := os.Open(p.self + "/mountinfo"); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else {
|
||||
d := vfs.NewMountInfoDecoder(r)
|
||||
err0 := f(d)
|
||||
if err = r.Close(); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot close mountinfo:")
|
||||
} else if err = d.Err(); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot parse mountinfo:")
|
||||
}
|
||||
return err0
|
||||
}
|
||||
}
|
||||
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);
|
||||
188
container/seccomp/libseccomp.go
Normal file
188
container/seccomp/libseccomp.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package seccomp
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: --static libseccomp
|
||||
|
||||
#include <libseccomp-helper.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -4,22 +4,22 @@ import (
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"slices"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||
. "hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
func TestExport(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts seccomp.SyscallOpts
|
||||
presets FilterPreset
|
||||
flags ExportFlag
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{"compat", 0, []byte{
|
||||
{"compat", 0, 0, []byte{
|
||||
0x95, 0xec, 0x69, 0xd0, 0x17, 0x73, 0x3e, 0x07,
|
||||
0x21, 0x60, 0xe0, 0xda, 0x80, 0xfd, 0xeb, 0xec,
|
||||
0xdf, 0x27, 0xae, 0x81, 0x66, 0xf5, 0xe2, 0xa7,
|
||||
@@ -29,7 +29,7 @@ func TestExport(t *testing.T) {
|
||||
0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a,
|
||||
0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5,
|
||||
}, false},
|
||||
{"base", seccomp.FlagExt, []byte{
|
||||
{"base", PresetExt, 0, []byte{
|
||||
0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79,
|
||||
0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54,
|
||||
0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d,
|
||||
@@ -39,10 +39,10 @@ func TestExport(t *testing.T) {
|
||||
0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59,
|
||||
0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d,
|
||||
}, false},
|
||||
{"everything", seccomp.FlagExt |
|
||||
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel |
|
||||
seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan |
|
||||
seccomp.FlagBluetooth, []byte{
|
||||
{"everything", PresetExt |
|
||||
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
|
||||
PresetLinux32, AllowMultiarch | AllowCAN |
|
||||
AllowBluetooth, []byte{
|
||||
0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34,
|
||||
0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57,
|
||||
0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f,
|
||||
@@ -52,8 +52,7 @@ func TestExport(t *testing.T) {
|
||||
0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69,
|
||||
0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb,
|
||||
}, false},
|
||||
{"strict", seccomp.FlagExt |
|
||||
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{
|
||||
{"strict", PresetStrict, 0, []byte{
|
||||
0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51,
|
||||
0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c,
|
||||
0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c,
|
||||
@@ -64,7 +63,7 @@ func TestExport(t *testing.T) {
|
||||
0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35,
|
||||
}, false},
|
||||
{"strict compat", 0 |
|
||||
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{
|
||||
PresetDenyNS | PresetDenyTTY | PresetDenyDevel, 0, []byte{
|
||||
0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9,
|
||||
0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e,
|
||||
0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5,
|
||||
@@ -74,15 +73,22 @@ func TestExport(t *testing.T) {
|
||||
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
|
||||
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
|
||||
}, false},
|
||||
{"hakurei default", PresetExt | PresetDenyDevel, 0, []byte{
|
||||
0xc6, 0x98, 0xb0, 0x81, 0xff, 0x95, 0x7a, 0xfe,
|
||||
0x17, 0xa6, 0xd9, 0x43, 0x74, 0x53, 0x7d, 0x37,
|
||||
0xf2, 0xa6, 0x3f, 0x6f, 0x9d, 0xd7, 0x5d, 0xa7,
|
||||
0x54, 0x65, 0x42, 0x40, 0x7a, 0x9e, 0x32, 0x47,
|
||||
0x6e, 0xbd, 0xa3, 0x31, 0x2b, 0xa7, 0x78, 0x5d,
|
||||
0x7f, 0x61, 0x85, 0x42, 0xbc, 0xfa, 0xf2, 0x7c,
|
||||
0xa2, 0x7d, 0xcc, 0x2d, 0xdd, 0xba, 0x85, 0x20,
|
||||
0x69, 0xd2, 0x8b, 0xcf, 0xe8, 0xca, 0xd3, 0x9a,
|
||||
}, false},
|
||||
}
|
||||
|
||||
buf := make([]byte, 8)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
seccomp.CPrintln = log.Println
|
||||
t.Cleanup(func() { seccomp.CPrintln = nil })
|
||||
|
||||
e := seccomp.New(tc.opts)
|
||||
e := New(Preset(tc.presets, tc.flags), tc.flags)
|
||||
digest := sha512.New()
|
||||
|
||||
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
|
||||
@@ -91,9 +97,8 @@ func TestExport(t *testing.T) {
|
||||
}
|
||||
if err := e.Close(); err != nil {
|
||||
t.Errorf("Close: error = %v", err)
|
||||
return
|
||||
}
|
||||
if got := digest.Sum(nil); slices.Compare(got, tc.want) != 0 {
|
||||
if got := digest.Sum(nil); !slices.Equal(got, tc.want) {
|
||||
t.Fatalf("Export() hash = %x, want %x",
|
||||
got, tc.want)
|
||||
return
|
||||
@@ -102,7 +107,7 @@ func TestExport(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("close without use", func(t *testing.T) {
|
||||
e := seccomp.New(0)
|
||||
e := New(Preset(0, 0), 0)
|
||||
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
|
||||
t.Errorf("Close: error = %v", err)
|
||||
return
|
||||
@@ -110,12 +115,15 @@ func TestExport(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("close partial read", func(t *testing.T) {
|
||||
e := seccomp.New(0)
|
||||
if _, err := e.Read(make([]byte, 0)); err != nil {
|
||||
e := New(Preset(0, 0), 0)
|
||||
if _, err := e.Read(nil); err != nil {
|
||||
t.Errorf("Read: error = %v", err)
|
||||
return
|
||||
}
|
||||
if err := e.Close(); err == nil || err.Error() != "seccomp_export_bpf failed: operation canceled" {
|
||||
// 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
|
||||
}
|
||||
@@ -125,10 +133,10 @@ func TestExport(t *testing.T) {
|
||||
func BenchmarkExport(b *testing.B) {
|
||||
buf := make([]byte, 8)
|
||||
for i := 0; i < b.N; i++ {
|
||||
e := seccomp.New(seccomp.FlagExt |
|
||||
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel |
|
||||
seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan |
|
||||
seccomp.FlagBluetooth)
|
||||
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)
|
||||
}
|
||||
83
container/seccomp/mksysnum_linux.pl
Executable file
83
container/seccomp/mksysnum_linux.pl
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/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;
|
||||
|
||||
my $command = "mksysnum_linux.pl ". join(' ', @ARGV);
|
||||
|
||||
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 > 302){ # 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
|
||||
6
container/seccomp/presets_clone_generic.go
Normal file
6
container/seccomp/presets_clone_generic.go
Normal file
@@ -0,0 +1,6 @@
|
||||
//go:build !s390 && !s390x
|
||||
|
||||
package seccomp
|
||||
|
||||
/* Normally the flags come first */
|
||||
const cloneArg = 0
|
||||
@@ -5,11 +5,18 @@ import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||
"hakurei.app/helper/proc"
|
||||
)
|
||||
|
||||
const (
|
||||
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
|
||||
)
|
||||
|
||||
// New returns an inactive Encoder instance.
|
||||
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
|
||||
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }
|
||||
|
||||
// Load loads a filter into the kernel.
|
||||
func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) }
|
||||
|
||||
/*
|
||||
An Encoder writes a BPF program to an output stream.
|
||||
@@ -39,17 +46,20 @@ func (e *Encoder) Close() error {
|
||||
}
|
||||
|
||||
// NewFile returns an instance of exporter implementing [proc.File].
|
||||
func NewFile(opts SyscallOpts) proc.File { return &File{opts: opts} }
|
||||
func NewFile(rules []NativeRule, flags ExportFlag) proc.File {
|
||||
return &File{rules: rules, flags: flags}
|
||||
}
|
||||
|
||||
// File implements [proc.File] and provides access to the read end of exporter pipe.
|
||||
type File struct {
|
||||
opts SyscallOpts
|
||||
rules []NativeRule
|
||||
flags ExportFlag
|
||||
proc.BaseFile
|
||||
}
|
||||
|
||||
func (f *File) ErrCount() int { return 2 }
|
||||
func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error {
|
||||
e := newExporter(f.opts)
|
||||
e := newExporter(f.rules, f.flags)
|
||||
if err := e.prepare(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package seccomp provides high level wrappers around libseccomp.
|
||||
package seccomp
|
||||
|
||||
import (
|
||||
@@ -7,8 +8,9 @@ import (
|
||||
)
|
||||
|
||||
type exporter struct {
|
||||
opts SyscallOpts
|
||||
r, w *os.File
|
||||
rules []NativeRule
|
||||
flags ExportFlag
|
||||
r, w *os.File
|
||||
|
||||
prepareOnce sync.Once
|
||||
prepareErr error
|
||||
@@ -27,7 +29,12 @@ func (e *exporter) prepare() error {
|
||||
}
|
||||
|
||||
ec := make(chan error, 1)
|
||||
go func(fd uintptr) { ec <- exportFilter(fd, e.opts); close(ec); _ = e.closeWrite() }(e.w.Fd())
|
||||
go func(fd uintptr) {
|
||||
ec <- Export(int(fd), e.rules, e.flags)
|
||||
close(ec)
|
||||
_ = e.closeWrite()
|
||||
runtime.KeepAlive(e.w)
|
||||
}(e.w.Fd())
|
||||
e.exportErr = ec
|
||||
runtime.SetFinalizer(e, (*exporter).closeWrite)
|
||||
})
|
||||
@@ -48,6 +55,6 @@ func (e *exporter) closeWrite() error {
|
||||
return e.closeErr
|
||||
}
|
||||
|
||||
func newExporter(opts SyscallOpts) *exporter {
|
||||
return &exporter{opts: opts}
|
||||
func newExporter(rules []NativeRule, flags ExportFlag) *exporter {
|
||||
return &exporter{rules: rules, flags: flags}
|
||||
}
|
||||
65
container/seccomp/seccomp_test.go
Normal file
65
container/seccomp/seccomp_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package seccomp_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
func TestLibraryError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sample *seccomp.LibraryError
|
||||
want string
|
||||
wantIs bool
|
||||
compare error
|
||||
}{
|
||||
{
|
||||
"full",
|
||||
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
|
||||
"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
|
||||
true,
|
||||
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
|
||||
},
|
||||
{
|
||||
"errno only",
|
||||
&seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
|
||||
"seccomp_init failed: cannot allocate memory",
|
||||
false,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"seccomp only",
|
||||
&seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
|
||||
"internal libseccomp failure: bad address",
|
||||
true,
|
||||
syscall.EFAULT,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
|
||||
t.Errorf("errors.Is(%#v, %#v) did not return %v",
|
||||
tc.sample, tc.compare, tc.wantIs)
|
||||
}
|
||||
|
||||
if got := tc.sample.Error(); got != tc.want {
|
||||
t.Errorf("Error: %q, want %q",
|
||||
got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
wantPanic := "invalid libseccomp error"
|
||||
defer func() {
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("panic: %q, want %q", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
runtime.KeepAlive(new(seccomp.LibraryError).Error())
|
||||
})
|
||||
}
|
||||
28
container/seccomp/syscall.go
Normal file
28
container/seccomp/syscall.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package seccomp
|
||||
|
||||
import "iter"
|
||||
|
||||
// Syscalls returns an iterator over all wired syscalls.
|
||||
func Syscalls() iter.Seq2[string, int] {
|
||||
return func(yield func(string, int) bool) {
|
||||
for name, num := range syscallNum {
|
||||
if !yield(name, num) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for name, num := range syscallNumExtra {
|
||||
if !yield(name, num) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SyscallResolveName resolves a syscall number from its string representation.
|
||||
func SyscallResolveName(name string) (num int, ok bool) {
|
||||
if num, ok = syscallNum[name]; ok {
|
||||
return
|
||||
}
|
||||
num, ok = syscallNumExtra[name]
|
||||
return
|
||||
}
|
||||
54
container/seccomp/syscall_extra_linux_amd64.go
Normal file
54
container/seccomp/syscall_extra_linux_amd64.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package seccomp
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: --static libseccomp
|
||||
|
||||
#include <seccomp.h>
|
||||
#include <sys/personality.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const (
|
||||
PER_LINUX = C.PER_LINUX
|
||||
PER_LINUX32 = C.PER_LINUX32
|
||||
)
|
||||
|
||||
var syscallNumExtra = map[string]int{
|
||||
"umount": SYS_UMOUNT,
|
||||
"subpage_prot": SYS_SUBPAGE_PROT,
|
||||
"switch_endian": SYS_SWITCH_ENDIAN,
|
||||
"vm86": SYS_VM86,
|
||||
"vm86old": SYS_VM86OLD,
|
||||
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
|
||||
"clock_settime64": SYS_CLOCK_SETTIME64,
|
||||
"chown32": SYS_CHOWN32,
|
||||
"fchown32": SYS_FCHOWN32,
|
||||
"lchown32": SYS_LCHOWN32,
|
||||
"setgid32": SYS_SETGID32,
|
||||
"setgroups32": SYS_SETGROUPS32,
|
||||
"setregid32": SYS_SETREGID32,
|
||||
"setresgid32": SYS_SETRESGID32,
|
||||
"setresuid32": SYS_SETRESUID32,
|
||||
"setreuid32": SYS_SETREUID32,
|
||||
"setuid32": SYS_SETUID32,
|
||||
}
|
||||
|
||||
const (
|
||||
SYS_UMOUNT = C.__SNR_umount
|
||||
SYS_SUBPAGE_PROT = C.__SNR_subpage_prot
|
||||
SYS_SWITCH_ENDIAN = C.__SNR_switch_endian
|
||||
SYS_VM86 = C.__SNR_vm86
|
||||
SYS_VM86OLD = C.__SNR_vm86old
|
||||
SYS_CLOCK_ADJTIME64 = C.__SNR_clock_adjtime64
|
||||
SYS_CLOCK_SETTIME64 = C.__SNR_clock_settime64
|
||||
SYS_CHOWN32 = C.__SNR_chown32
|
||||
SYS_FCHOWN32 = C.__SNR_fchown32
|
||||
SYS_LCHOWN32 = C.__SNR_lchown32
|
||||
SYS_SETGID32 = C.__SNR_setgid32
|
||||
SYS_SETGROUPS32 = C.__SNR_setgroups32
|
||||
SYS_SETREGID32 = C.__SNR_setregid32
|
||||
SYS_SETRESGID32 = C.__SNR_setresgid32
|
||||
SYS_SETRESUID32 = C.__SNR_setresuid32
|
||||
SYS_SETREUID32 = C.__SNR_setreuid32
|
||||
SYS_SETUID32 = C.__SNR_setuid32
|
||||
)
|
||||
459
container/seccomp/syscall_linux_amd64.go
Normal file
459
container/seccomp/syscall_linux_amd64.go
Normal file
@@ -0,0 +1,459 @@
|
||||
// mksysnum_linux.pl /usr/include/asm/unistd_64.h
|
||||
// Code generated by the command above; DO NOT EDIT.
|
||||
|
||||
package seccomp
|
||||
|
||||
import . "syscall"
|
||||
|
||||
var syscallNum = map[string]int{
|
||||
"read": SYS_READ,
|
||||
"write": SYS_WRITE,
|
||||
"open": SYS_OPEN,
|
||||
"close": SYS_CLOSE,
|
||||
"stat": SYS_STAT,
|
||||
"fstat": SYS_FSTAT,
|
||||
"lstat": SYS_LSTAT,
|
||||
"poll": SYS_POLL,
|
||||
"lseek": SYS_LSEEK,
|
||||
"mmap": SYS_MMAP,
|
||||
"mprotect": SYS_MPROTECT,
|
||||
"munmap": SYS_MUNMAP,
|
||||
"brk": SYS_BRK,
|
||||
"rt_sigaction": SYS_RT_SIGACTION,
|
||||
"rt_sigprocmask": SYS_RT_SIGPROCMASK,
|
||||
"rt_sigreturn": SYS_RT_SIGRETURN,
|
||||
"ioctl": SYS_IOCTL,
|
||||
"pread64": SYS_PREAD64,
|
||||
"pwrite64": SYS_PWRITE64,
|
||||
"readv": SYS_READV,
|
||||
"writev": SYS_WRITEV,
|
||||
"access": SYS_ACCESS,
|
||||
"pipe": SYS_PIPE,
|
||||
"select": SYS_SELECT,
|
||||
"sched_yield": SYS_SCHED_YIELD,
|
||||
"mremap": SYS_MREMAP,
|
||||
"msync": SYS_MSYNC,
|
||||
"mincore": SYS_MINCORE,
|
||||
"madvise": SYS_MADVISE,
|
||||
"shmget": SYS_SHMGET,
|
||||
"shmat": SYS_SHMAT,
|
||||
"shmctl": SYS_SHMCTL,
|
||||
"dup": SYS_DUP,
|
||||
"dup2": SYS_DUP2,
|
||||
"pause": SYS_PAUSE,
|
||||
"nanosleep": SYS_NANOSLEEP,
|
||||
"getitimer": SYS_GETITIMER,
|
||||
"alarm": SYS_ALARM,
|
||||
"setitimer": SYS_SETITIMER,
|
||||
"getpid": SYS_GETPID,
|
||||
"sendfile": SYS_SENDFILE,
|
||||
"socket": SYS_SOCKET,
|
||||
"connect": SYS_CONNECT,
|
||||
"accept": SYS_ACCEPT,
|
||||
"sendto": SYS_SENDTO,
|
||||
"recvfrom": SYS_RECVFROM,
|
||||
"sendmsg": SYS_SENDMSG,
|
||||
"recvmsg": SYS_RECVMSG,
|
||||
"shutdown": SYS_SHUTDOWN,
|
||||
"bind": SYS_BIND,
|
||||
"listen": SYS_LISTEN,
|
||||
"getsockname": SYS_GETSOCKNAME,
|
||||
"getpeername": SYS_GETPEERNAME,
|
||||
"socketpair": SYS_SOCKETPAIR,
|
||||
"setsockopt": SYS_SETSOCKOPT,
|
||||
"getsockopt": SYS_GETSOCKOPT,
|
||||
"clone": SYS_CLONE,
|
||||
"fork": SYS_FORK,
|
||||
"vfork": SYS_VFORK,
|
||||
"execve": SYS_EXECVE,
|
||||
"exit": SYS_EXIT,
|
||||
"wait4": SYS_WAIT4,
|
||||
"kill": SYS_KILL,
|
||||
"uname": SYS_UNAME,
|
||||
"semget": SYS_SEMGET,
|
||||
"semop": SYS_SEMOP,
|
||||
"semctl": SYS_SEMCTL,
|
||||
"shmdt": SYS_SHMDT,
|
||||
"msgget": SYS_MSGGET,
|
||||
"msgsnd": SYS_MSGSND,
|
||||
"msgrcv": SYS_MSGRCV,
|
||||
"msgctl": SYS_MSGCTL,
|
||||
"fcntl": SYS_FCNTL,
|
||||
"flock": SYS_FLOCK,
|
||||
"fsync": SYS_FSYNC,
|
||||
"fdatasync": SYS_FDATASYNC,
|
||||
"truncate": SYS_TRUNCATE,
|
||||
"ftruncate": SYS_FTRUNCATE,
|
||||
"getdents": SYS_GETDENTS,
|
||||
"getcwd": SYS_GETCWD,
|
||||
"chdir": SYS_CHDIR,
|
||||
"fchdir": SYS_FCHDIR,
|
||||
"rename": SYS_RENAME,
|
||||
"mkdir": SYS_MKDIR,
|
||||
"rmdir": SYS_RMDIR,
|
||||
"creat": SYS_CREAT,
|
||||
"link": SYS_LINK,
|
||||
"unlink": SYS_UNLINK,
|
||||
"symlink": SYS_SYMLINK,
|
||||
"readlink": SYS_READLINK,
|
||||
"chmod": SYS_CHMOD,
|
||||
"fchmod": SYS_FCHMOD,
|
||||
"chown": SYS_CHOWN,
|
||||
"fchown": SYS_FCHOWN,
|
||||
"lchown": SYS_LCHOWN,
|
||||
"umask": SYS_UMASK,
|
||||
"gettimeofday": SYS_GETTIMEOFDAY,
|
||||
"getrlimit": SYS_GETRLIMIT,
|
||||
"getrusage": SYS_GETRUSAGE,
|
||||
"sysinfo": SYS_SYSINFO,
|
||||
"times": SYS_TIMES,
|
||||
"ptrace": SYS_PTRACE,
|
||||
"getuid": SYS_GETUID,
|
||||
"syslog": SYS_SYSLOG,
|
||||
"getgid": SYS_GETGID,
|
||||
"setuid": SYS_SETUID,
|
||||
"setgid": SYS_SETGID,
|
||||
"geteuid": SYS_GETEUID,
|
||||
"getegid": SYS_GETEGID,
|
||||
"setpgid": SYS_SETPGID,
|
||||
"getppid": SYS_GETPPID,
|
||||
"getpgrp": SYS_GETPGRP,
|
||||
"setsid": SYS_SETSID,
|
||||
"setreuid": SYS_SETREUID,
|
||||
"setregid": SYS_SETREGID,
|
||||
"getgroups": SYS_GETGROUPS,
|
||||
"setgroups": SYS_SETGROUPS,
|
||||
"setresuid": SYS_SETRESUID,
|
||||
"getresuid": SYS_GETRESUID,
|
||||
"setresgid": SYS_SETRESGID,
|
||||
"getresgid": SYS_GETRESGID,
|
||||
"getpgid": SYS_GETPGID,
|
||||
"setfsuid": SYS_SETFSUID,
|
||||
"setfsgid": SYS_SETFSGID,
|
||||
"getsid": SYS_GETSID,
|
||||
"capget": SYS_CAPGET,
|
||||
"capset": SYS_CAPSET,
|
||||
"rt_sigpending": SYS_RT_SIGPENDING,
|
||||
"rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT,
|
||||
"rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO,
|
||||
"rt_sigsuspend": SYS_RT_SIGSUSPEND,
|
||||
"sigaltstack": SYS_SIGALTSTACK,
|
||||
"utime": SYS_UTIME,
|
||||
"mknod": SYS_MKNOD,
|
||||
"uselib": SYS_USELIB,
|
||||
"personality": SYS_PERSONALITY,
|
||||
"ustat": SYS_USTAT,
|
||||
"statfs": SYS_STATFS,
|
||||
"fstatfs": SYS_FSTATFS,
|
||||
"sysfs": SYS_SYSFS,
|
||||
"getpriority": SYS_GETPRIORITY,
|
||||
"setpriority": SYS_SETPRIORITY,
|
||||
"sched_setparam": SYS_SCHED_SETPARAM,
|
||||
"sched_getparam": SYS_SCHED_GETPARAM,
|
||||
"sched_setscheduler": SYS_SCHED_SETSCHEDULER,
|
||||
"sched_getscheduler": SYS_SCHED_GETSCHEDULER,
|
||||
"sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX,
|
||||
"sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN,
|
||||
"sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL,
|
||||
"mlock": SYS_MLOCK,
|
||||
"munlock": SYS_MUNLOCK,
|
||||
"mlockall": SYS_MLOCKALL,
|
||||
"munlockall": SYS_MUNLOCKALL,
|
||||
"vhangup": SYS_VHANGUP,
|
||||
"modify_ldt": SYS_MODIFY_LDT,
|
||||
"pivot_root": SYS_PIVOT_ROOT,
|
||||
"_sysctl": SYS__SYSCTL,
|
||||
"prctl": SYS_PRCTL,
|
||||
"arch_prctl": SYS_ARCH_PRCTL,
|
||||
"adjtimex": SYS_ADJTIMEX,
|
||||
"setrlimit": SYS_SETRLIMIT,
|
||||
"chroot": SYS_CHROOT,
|
||||
"sync": SYS_SYNC,
|
||||
"acct": SYS_ACCT,
|
||||
"settimeofday": SYS_SETTIMEOFDAY,
|
||||
"mount": SYS_MOUNT,
|
||||
"umount2": SYS_UMOUNT2,
|
||||
"swapon": SYS_SWAPON,
|
||||
"swapoff": SYS_SWAPOFF,
|
||||
"reboot": SYS_REBOOT,
|
||||
"sethostname": SYS_SETHOSTNAME,
|
||||
"setdomainname": SYS_SETDOMAINNAME,
|
||||
"iopl": SYS_IOPL,
|
||||
"ioperm": SYS_IOPERM,
|
||||
"create_module": SYS_CREATE_MODULE,
|
||||
"init_module": SYS_INIT_MODULE,
|
||||
"delete_module": SYS_DELETE_MODULE,
|
||||
"get_kernel_syms": SYS_GET_KERNEL_SYMS,
|
||||
"query_module": SYS_QUERY_MODULE,
|
||||
"quotactl": SYS_QUOTACTL,
|
||||
"nfsservctl": SYS_NFSSERVCTL,
|
||||
"getpmsg": SYS_GETPMSG,
|
||||
"putpmsg": SYS_PUTPMSG,
|
||||
"afs_syscall": SYS_AFS_SYSCALL,
|
||||
"tuxcall": SYS_TUXCALL,
|
||||
"security": SYS_SECURITY,
|
||||
"gettid": SYS_GETTID,
|
||||
"readahead": SYS_READAHEAD,
|
||||
"setxattr": SYS_SETXATTR,
|
||||
"lsetxattr": SYS_LSETXATTR,
|
||||
"fsetxattr": SYS_FSETXATTR,
|
||||
"getxattr": SYS_GETXATTR,
|
||||
"lgetxattr": SYS_LGETXATTR,
|
||||
"fgetxattr": SYS_FGETXATTR,
|
||||
"listxattr": SYS_LISTXATTR,
|
||||
"llistxattr": SYS_LLISTXATTR,
|
||||
"flistxattr": SYS_FLISTXATTR,
|
||||
"removexattr": SYS_REMOVEXATTR,
|
||||
"lremovexattr": SYS_LREMOVEXATTR,
|
||||
"fremovexattr": SYS_FREMOVEXATTR,
|
||||
"tkill": SYS_TKILL,
|
||||
"time": SYS_TIME,
|
||||
"futex": SYS_FUTEX,
|
||||
"sched_setaffinity": SYS_SCHED_SETAFFINITY,
|
||||
"sched_getaffinity": SYS_SCHED_GETAFFINITY,
|
||||
"set_thread_area": SYS_SET_THREAD_AREA,
|
||||
"io_setup": SYS_IO_SETUP,
|
||||
"io_destroy": SYS_IO_DESTROY,
|
||||
"io_getevents": SYS_IO_GETEVENTS,
|
||||
"io_submit": SYS_IO_SUBMIT,
|
||||
"io_cancel": SYS_IO_CANCEL,
|
||||
"get_thread_area": SYS_GET_THREAD_AREA,
|
||||
"lookup_dcookie": SYS_LOOKUP_DCOOKIE,
|
||||
"epoll_create": SYS_EPOLL_CREATE,
|
||||
"epoll_ctl_old": SYS_EPOLL_CTL_OLD,
|
||||
"epoll_wait_old": SYS_EPOLL_WAIT_OLD,
|
||||
"remap_file_pages": SYS_REMAP_FILE_PAGES,
|
||||
"getdents64": SYS_GETDENTS64,
|
||||
"set_tid_address": SYS_SET_TID_ADDRESS,
|
||||
"restart_syscall": SYS_RESTART_SYSCALL,
|
||||
"semtimedop": SYS_SEMTIMEDOP,
|
||||
"fadvise64": SYS_FADVISE64,
|
||||
"timer_create": SYS_TIMER_CREATE,
|
||||
"timer_settime": SYS_TIMER_SETTIME,
|
||||
"timer_gettime": SYS_TIMER_GETTIME,
|
||||
"timer_getoverrun": SYS_TIMER_GETOVERRUN,
|
||||
"timer_delete": SYS_TIMER_DELETE,
|
||||
"clock_settime": SYS_CLOCK_SETTIME,
|
||||
"clock_gettime": SYS_CLOCK_GETTIME,
|
||||
"clock_getres": SYS_CLOCK_GETRES,
|
||||
"clock_nanosleep": SYS_CLOCK_NANOSLEEP,
|
||||
"exit_group": SYS_EXIT_GROUP,
|
||||
"epoll_wait": SYS_EPOLL_WAIT,
|
||||
"epoll_ctl": SYS_EPOLL_CTL,
|
||||
"tgkill": SYS_TGKILL,
|
||||
"utimes": SYS_UTIMES,
|
||||
"vserver": SYS_VSERVER,
|
||||
"mbind": SYS_MBIND,
|
||||
"set_mempolicy": SYS_SET_MEMPOLICY,
|
||||
"get_mempolicy": SYS_GET_MEMPOLICY,
|
||||
"mq_open": SYS_MQ_OPEN,
|
||||
"mq_unlink": SYS_MQ_UNLINK,
|
||||
"mq_timedsend": SYS_MQ_TIMEDSEND,
|
||||
"mq_timedreceive": SYS_MQ_TIMEDRECEIVE,
|
||||
"mq_notify": SYS_MQ_NOTIFY,
|
||||
"mq_getsetattr": SYS_MQ_GETSETATTR,
|
||||
"kexec_load": SYS_KEXEC_LOAD,
|
||||
"waitid": SYS_WAITID,
|
||||
"add_key": SYS_ADD_KEY,
|
||||
"request_key": SYS_REQUEST_KEY,
|
||||
"keyctl": SYS_KEYCTL,
|
||||
"ioprio_set": SYS_IOPRIO_SET,
|
||||
"ioprio_get": SYS_IOPRIO_GET,
|
||||
"inotify_init": SYS_INOTIFY_INIT,
|
||||
"inotify_add_watch": SYS_INOTIFY_ADD_WATCH,
|
||||
"inotify_rm_watch": SYS_INOTIFY_RM_WATCH,
|
||||
"migrate_pages": SYS_MIGRATE_PAGES,
|
||||
"openat": SYS_OPENAT,
|
||||
"mkdirat": SYS_MKDIRAT,
|
||||
"mknodat": SYS_MKNODAT,
|
||||
"fchownat": SYS_FCHOWNAT,
|
||||
"futimesat": SYS_FUTIMESAT,
|
||||
"newfstatat": SYS_NEWFSTATAT,
|
||||
"unlinkat": SYS_UNLINKAT,
|
||||
"renameat": SYS_RENAMEAT,
|
||||
"linkat": SYS_LINKAT,
|
||||
"symlinkat": SYS_SYMLINKAT,
|
||||
"readlinkat": SYS_READLINKAT,
|
||||
"fchmodat": SYS_FCHMODAT,
|
||||
"faccessat": SYS_FACCESSAT,
|
||||
"pselect6": SYS_PSELECT6,
|
||||
"ppoll": SYS_PPOLL,
|
||||
"unshare": SYS_UNSHARE,
|
||||
"set_robust_list": SYS_SET_ROBUST_LIST,
|
||||
"get_robust_list": SYS_GET_ROBUST_LIST,
|
||||
"splice": SYS_SPLICE,
|
||||
"tee": SYS_TEE,
|
||||
"sync_file_range": SYS_SYNC_FILE_RANGE,
|
||||
"vmsplice": SYS_VMSPLICE,
|
||||
"move_pages": SYS_MOVE_PAGES,
|
||||
"utimensat": SYS_UTIMENSAT,
|
||||
"epoll_pwait": SYS_EPOLL_PWAIT,
|
||||
"signalfd": SYS_SIGNALFD,
|
||||
"timerfd_create": SYS_TIMERFD_CREATE,
|
||||
"eventfd": SYS_EVENTFD,
|
||||
"fallocate": SYS_FALLOCATE,
|
||||
"timerfd_settime": SYS_TIMERFD_SETTIME,
|
||||
"timerfd_gettime": SYS_TIMERFD_GETTIME,
|
||||
"accept4": SYS_ACCEPT4,
|
||||
"signalfd4": SYS_SIGNALFD4,
|
||||
"eventfd2": SYS_EVENTFD2,
|
||||
"epoll_create1": SYS_EPOLL_CREATE1,
|
||||
"dup3": SYS_DUP3,
|
||||
"pipe2": SYS_PIPE2,
|
||||
"inotify_init1": SYS_INOTIFY_INIT1,
|
||||
"preadv": SYS_PREADV,
|
||||
"pwritev": SYS_PWRITEV,
|
||||
"rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO,
|
||||
"perf_event_open": SYS_PERF_EVENT_OPEN,
|
||||
"recvmmsg": SYS_RECVMMSG,
|
||||
"fanotify_init": SYS_FANOTIFY_INIT,
|
||||
"fanotify_mark": SYS_FANOTIFY_MARK,
|
||||
"prlimit64": SYS_PRLIMIT64,
|
||||
"name_to_handle_at": SYS_NAME_TO_HANDLE_AT,
|
||||
"open_by_handle_at": SYS_OPEN_BY_HANDLE_AT,
|
||||
"clock_adjtime": SYS_CLOCK_ADJTIME,
|
||||
"syncfs": SYS_SYNCFS,
|
||||
"sendmmsg": SYS_SENDMMSG,
|
||||
"setns": SYS_SETNS,
|
||||
"getcpu": SYS_GETCPU,
|
||||
"process_vm_readv": SYS_PROCESS_VM_READV,
|
||||
"process_vm_writev": SYS_PROCESS_VM_WRITEV,
|
||||
"kcmp": SYS_KCMP,
|
||||
"finit_module": SYS_FINIT_MODULE,
|
||||
"sched_setattr": SYS_SCHED_SETATTR,
|
||||
"sched_getattr": SYS_SCHED_GETATTR,
|
||||
"renameat2": SYS_RENAMEAT2,
|
||||
"seccomp": SYS_SECCOMP,
|
||||
"getrandom": SYS_GETRANDOM,
|
||||
"memfd_create": SYS_MEMFD_CREATE,
|
||||
"kexec_file_load": SYS_KEXEC_FILE_LOAD,
|
||||
"bpf": SYS_BPF,
|
||||
"execveat": SYS_EXECVEAT,
|
||||
"userfaultfd": SYS_USERFAULTFD,
|
||||
"membarrier": SYS_MEMBARRIER,
|
||||
"mlock2": SYS_MLOCK2,
|
||||
"copy_file_range": SYS_COPY_FILE_RANGE,
|
||||
"preadv2": SYS_PREADV2,
|
||||
"pwritev2": SYS_PWRITEV2,
|
||||
"pkey_mprotect": SYS_PKEY_MPROTECT,
|
||||
"pkey_alloc": SYS_PKEY_ALLOC,
|
||||
"pkey_free": SYS_PKEY_FREE,
|
||||
"statx": SYS_STATX,
|
||||
"io_pgetevents": SYS_IO_PGETEVENTS,
|
||||
"rseq": SYS_RSEQ,
|
||||
"uretprobe": SYS_URETPROBE,
|
||||
"pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL,
|
||||
"io_uring_setup": SYS_IO_URING_SETUP,
|
||||
"io_uring_enter": SYS_IO_URING_ENTER,
|
||||
"io_uring_register": SYS_IO_URING_REGISTER,
|
||||
"open_tree": SYS_OPEN_TREE,
|
||||
"move_mount": SYS_MOVE_MOUNT,
|
||||
"fsopen": SYS_FSOPEN,
|
||||
"fsconfig": SYS_FSCONFIG,
|
||||
"fsmount": SYS_FSMOUNT,
|
||||
"fspick": SYS_FSPICK,
|
||||
"pidfd_open": SYS_PIDFD_OPEN,
|
||||
"clone3": SYS_CLONE3,
|
||||
"close_range": SYS_CLOSE_RANGE,
|
||||
"openat2": SYS_OPENAT2,
|
||||
"pidfd_getfd": SYS_PIDFD_GETFD,
|
||||
"faccessat2": SYS_FACCESSAT2,
|
||||
"process_madvise": SYS_PROCESS_MADVISE,
|
||||
"epoll_pwait2": SYS_EPOLL_PWAIT2,
|
||||
"mount_setattr": SYS_MOUNT_SETATTR,
|
||||
"quotactl_fd": SYS_QUOTACTL_FD,
|
||||
"landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET,
|
||||
"landlock_add_rule": SYS_LANDLOCK_ADD_RULE,
|
||||
"landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF,
|
||||
"memfd_secret": SYS_MEMFD_SECRET,
|
||||
"process_mrelease": SYS_PROCESS_MRELEASE,
|
||||
"futex_waitv": SYS_FUTEX_WAITV,
|
||||
"set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE,
|
||||
"cachestat": SYS_CACHESTAT,
|
||||
"fchmodat2": SYS_FCHMODAT2,
|
||||
"map_shadow_stack": SYS_MAP_SHADOW_STACK,
|
||||
"futex_wake": SYS_FUTEX_WAKE,
|
||||
"futex_wait": SYS_FUTEX_WAIT,
|
||||
"futex_requeue": SYS_FUTEX_REQUEUE,
|
||||
"statmount": SYS_STATMOUNT,
|
||||
"listmount": SYS_LISTMOUNT,
|
||||
"lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR,
|
||||
"lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR,
|
||||
"lsm_list_modules": SYS_LSM_LIST_MODULES,
|
||||
"mseal": SYS_MSEAL,
|
||||
}
|
||||
|
||||
const (
|
||||
SYS_NAME_TO_HANDLE_AT = 303
|
||||
SYS_OPEN_BY_HANDLE_AT = 304
|
||||
SYS_CLOCK_ADJTIME = 305
|
||||
SYS_SYNCFS = 306
|
||||
SYS_SENDMMSG = 307
|
||||
SYS_SETNS = 308
|
||||
SYS_GETCPU = 309
|
||||
SYS_PROCESS_VM_READV = 310
|
||||
SYS_PROCESS_VM_WRITEV = 311
|
||||
SYS_KCMP = 312
|
||||
SYS_FINIT_MODULE = 313
|
||||
SYS_SCHED_SETATTR = 314
|
||||
SYS_SCHED_GETATTR = 315
|
||||
SYS_RENAMEAT2 = 316
|
||||
SYS_SECCOMP = 317
|
||||
SYS_GETRANDOM = 318
|
||||
SYS_MEMFD_CREATE = 319
|
||||
SYS_KEXEC_FILE_LOAD = 320
|
||||
SYS_BPF = 321
|
||||
SYS_EXECVEAT = 322
|
||||
SYS_USERFAULTFD = 323
|
||||
SYS_MEMBARRIER = 324
|
||||
SYS_MLOCK2 = 325
|
||||
SYS_COPY_FILE_RANGE = 326
|
||||
SYS_PREADV2 = 327
|
||||
SYS_PWRITEV2 = 328
|
||||
SYS_PKEY_MPROTECT = 329
|
||||
SYS_PKEY_ALLOC = 330
|
||||
SYS_PKEY_FREE = 331
|
||||
SYS_STATX = 332
|
||||
SYS_IO_PGETEVENTS = 333
|
||||
SYS_RSEQ = 334
|
||||
SYS_URETPROBE = 335
|
||||
SYS_PIDFD_SEND_SIGNAL = 424
|
||||
SYS_IO_URING_SETUP = 425
|
||||
SYS_IO_URING_ENTER = 426
|
||||
SYS_IO_URING_REGISTER = 427
|
||||
SYS_OPEN_TREE = 428
|
||||
SYS_MOVE_MOUNT = 429
|
||||
SYS_FSOPEN = 430
|
||||
SYS_FSCONFIG = 431
|
||||
SYS_FSMOUNT = 432
|
||||
SYS_FSPICK = 433
|
||||
SYS_PIDFD_OPEN = 434
|
||||
SYS_CLONE3 = 435
|
||||
SYS_CLOSE_RANGE = 436
|
||||
SYS_OPENAT2 = 437
|
||||
SYS_PIDFD_GETFD = 438
|
||||
SYS_FACCESSAT2 = 439
|
||||
SYS_PROCESS_MADVISE = 440
|
||||
SYS_EPOLL_PWAIT2 = 441
|
||||
SYS_MOUNT_SETATTR = 442
|
||||
SYS_QUOTACTL_FD = 443
|
||||
SYS_LANDLOCK_CREATE_RULESET = 444
|
||||
SYS_LANDLOCK_ADD_RULE = 445
|
||||
SYS_LANDLOCK_RESTRICT_SELF = 446
|
||||
SYS_MEMFD_SECRET = 447
|
||||
SYS_PROCESS_MRELEASE = 448
|
||||
SYS_FUTEX_WAITV = 449
|
||||
SYS_SET_MEMPOLICY_HOME_NODE = 450
|
||||
SYS_CACHESTAT = 451
|
||||
SYS_FCHMODAT2 = 452
|
||||
SYS_MAP_SHADOW_STACK = 453
|
||||
SYS_FUTEX_WAKE = 454
|
||||
SYS_FUTEX_WAIT = 455
|
||||
SYS_FUTEX_REQUEUE = 456
|
||||
SYS_STATMOUNT = 457
|
||||
SYS_LISTMOUNT = 458
|
||||
SYS_LSM_GET_SELF_ATTR = 459
|
||||
SYS_LSM_SET_SELF_ATTR = 460
|
||||
SYS_LSM_LIST_MODULES = 461
|
||||
SYS_MSEAL = 462
|
||||
)
|
||||
20
container/seccomp/syscall_test.go
Normal file
20
container/seccomp/syscall_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package seccomp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSyscallResolveName(t *testing.T) {
|
||||
for name, want := range Syscalls() {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if got := syscallResolveName(name); got != want {
|
||||
t.Errorf("syscallResolveName(%q) = %d, want %d",
|
||||
name, got, want)
|
||||
}
|
||||
if got, ok := SyscallResolveName(name); !ok || got != want {
|
||||
t.Errorf("SyscallResolveName(%q) = %d, want %d",
|
||||
name, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
81
container/syscall.go
Normal file
81
container/syscall.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
O_PATH = 0x200000
|
||||
|
||||
PR_SET_NO_NEW_PRIVS = 0x26
|
||||
|
||||
CAP_SYS_ADMIN = 0x15
|
||||
CAP_SETPCAP = 0x8
|
||||
)
|
||||
|
||||
const (
|
||||
SUID_DUMP_DISABLE = iota
|
||||
SUID_DUMP_USER
|
||||
)
|
||||
|
||||
func SetDumpable(dumpable uintptr) error {
|
||||
// linux/sched/coredump.h
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
||||
|
||||
PR_CAP_AMBIENT = 0x2f
|
||||
PR_CAP_AMBIENT_RAISE = 0x2
|
||||
PR_CAP_AMBIENT_CLEAR_ALL = 0x4
|
||||
)
|
||||
|
||||
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 {
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET,
|
||||
uintptr(unsafe.Pointer(hdrp)),
|
||||
uintptr(unsafe.Pointer(&datap[0])), 0); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IgnoringEINTR makes a function call and repeats it if it returns an
|
||||
// EINTR error. This appears to be required even though we install all
|
||||
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
|
||||
// Also #20400 and #36644 are issues in which a signal handler is
|
||||
// installed without setting SA_RESTART. None of these are the common case,
|
||||
// but there are enough of them that it seems that we can't avoid
|
||||
// an EINTR loop.
|
||||
func IgnoringEINTR(fn func() error) error {
|
||||
for {
|
||||
err := fn()
|
||||
if err != syscall.EINTR {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
47
container/sysctl.go
Normal file
47
container/sysctl.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
kernelOverflowuid int
|
||||
kernelOverflowgid int
|
||||
kernelCapLastCap int
|
||||
|
||||
sysctlOnce sync.Once
|
||||
)
|
||||
|
||||
const (
|
||||
kernelOverflowuidPath = "/proc/sys/kernel/overflowuid"
|
||||
kernelOverflowgidPath = "/proc/sys/kernel/overflowgid"
|
||||
kernelCapLastCapPath = "/proc/sys/kernel/cap_last_cap"
|
||||
)
|
||||
|
||||
func mustReadSysctl() {
|
||||
if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
|
||||
log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
|
||||
} else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
|
||||
log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
|
||||
}
|
||||
|
||||
if v, err := os.ReadFile(kernelOverflowgidPath); err != nil {
|
||||
log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
|
||||
} else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
|
||||
log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
|
||||
}
|
||||
|
||||
if v, err := os.ReadFile(kernelCapLastCapPath); err != nil {
|
||||
log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
|
||||
} else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
|
||||
log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid }
|
||||
func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid }
|
||||
func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user