Compare commits
249 Commits
fsu-bypass
...
master
Author | SHA1 | Date | |
---|---|---|---|
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 |
@ -20,5 +20,5 @@ jobs:
|
|||||||
uses: https://gitea.com/actions/release-action@main
|
uses: https://gitea.com/actions/release-action@main
|
||||||
with:
|
with:
|
||||||
files: |-
|
files: |-
|
||||||
result/fortify-**
|
result/hakurei-**
|
||||||
api_key: '${{secrets.RELEASE_TOKEN}}'
|
api_key: '${{secrets.RELEASE_TOKEN}}'
|
||||||
|
@ -5,25 +5,107 @@ on:
|
|||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
hakurei:
|
||||||
name: Run NixOS test
|
name: Hakurei
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run NixOS test
|
||||||
run: |
|
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hakurei
|
||||||
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: Upload test output
|
- name: Upload test output
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: "nixos-vm-output"
|
name: "hakurei-vm-output"
|
||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
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:
|
dist:
|
||||||
name: Create distribution
|
name: Create distribution
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
@ -34,15 +116,15 @@ jobs:
|
|||||||
- name: Build for test
|
- name: Build for test
|
||||||
id: build-test
|
id: build-test
|
||||||
run: >-
|
run: >-
|
||||||
export FORTIFY_REV="$(git rev-parse --short HEAD)" &&
|
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
|
||||||
sed -i.old 's/version = /version = "0.0.0-'$FORTIFY_REV'"; # version = /' package.nix &&
|
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
|
||||||
nix build --print-out-paths --print-build-logs .#dist &&
|
nix build --print-out-paths --print-build-logs .#dist &&
|
||||||
mv package.nix.old package.nix &&
|
mv package.nix.old package.nix &&
|
||||||
echo "rev=$FORTIFY_REV" >> $GITHUB_OUTPUT
|
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload test build
|
- name: Upload test build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: "fortify-${{ steps.build-test.outputs.rev }}"
|
name: "hakurei-${{ steps.build-test.outputs.rev }}"
|
||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
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
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,7 +5,7 @@
|
|||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
*.pkg
|
*.pkg
|
||||||
/fortify
|
/hakurei
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
@ -29,4 +29,4 @@ go.work.sum
|
|||||||
security-context-v1-protocol.*
|
security-context-v1-protocol.*
|
||||||
|
|
||||||
# release
|
# release
|
||||||
/dist/fortify-*
|
/dist/hakurei-*
|
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)
|
<p align="center">
|
||||||
[](https://goreportcard.com/report/git.gensokyo.uk/security/fortify)
|
<a href="https://pkg.go.dev/git.gensokyo.uk/security/hakurei"><img src="https://pkg.go.dev/badge/git.gensokyo.uk/security/hakurei.svg" alt="Go Reference" /></a>
|
||||||
|
<a href="https://goreportcard.com/report/git.gensokyo.uk/security/hakurei"><img src="https://goreportcard.com/badge/git.gensokyo.uk/security/hakurei" alt="Go Report Card" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
Lets you run graphical applications as another user in a confined environment with a nice NixOS
|
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
|
||||||
module to configure target users and provide launchers and desktop files for your privileged user.
|
It 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.
|
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
|
||||||
|
|
||||||
- It protects applications from each other.
|
|
||||||
|
|
||||||
- It provides UID isolation on top of the standard application sandbox.
|
|
||||||
|
|
||||||
If you have a flakes-enabled nix environment, you can try out the tool by running:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
nix run git+https://git.gensokyo.uk/security/fortify -- help
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module usage
|
|
||||||
|
|
||||||
The NixOS module currently requires home-manager to function correctly.
|
|
||||||
|
|
||||||
Full module documentation can be found [here](options.md).
|
|
||||||
|
|
||||||
To use the module, import it into your configuration with
|
To use the module, import it into your configuration with
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||||
|
|
||||||
fortify = {
|
hakurei = {
|
||||||
url = "git+https://git.gensokyo.uk/security/fortify";
|
url = "git+https://git.gensokyo.uk/security/hakurei";
|
||||||
|
|
||||||
# Optional but recommended to limit the size of your system closure.
|
# Optional but recommended to limit the size of your system closure.
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, fortify, ... }:
|
outputs = { self, nixpkgs, hakurei, ... }:
|
||||||
{
|
{
|
||||||
nixosConfigurations.fortify = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
modules = [
|
modules = [
|
||||||
fortify.nixosModules.fortify
|
hakurei.nixosModules.hakurei
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This adds the `environment.fortify` option:
|
This adds the `environment.hakurei` option:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
environment.fortify = {
|
environment.hakurei = {
|
||||||
enable = true;
|
enable = true;
|
||||||
stateDir = "/var/lib/persist/module/fortify";
|
stateDir = "/var/lib/hakurei";
|
||||||
users = {
|
users = {
|
||||||
alice = 0;
|
alice = 0;
|
||||||
nixos = 10;
|
nixos = 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
apps = [
|
commonPaths = [
|
||||||
{
|
{
|
||||||
|
src = "/sdcard";
|
||||||
|
write = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
extraHomeConfig = {
|
||||||
|
home.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
|
||||||
|
apps = {
|
||||||
|
"org.chromium.Chromium" = {
|
||||||
name = "chromium";
|
name = "chromium";
|
||||||
id = "org.chromium.Chromium";
|
identity = 1;
|
||||||
packages = [ pkgs.chromium ];
|
packages = [ pkgs.chromium ];
|
||||||
userns = true;
|
userns = true;
|
||||||
mapRealUid = true;
|
mapRealUid = true;
|
||||||
@ -104,16 +106,20 @@ This adds the `environment.fortify` option:
|
|||||||
broadcast = { };
|
broadcast = { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
{
|
|
||||||
|
"org.claws_mail.Claws-Mail" = {
|
||||||
name = "claws-mail";
|
name = "claws-mail";
|
||||||
id = "org.claws_mail.Claws-Mail";
|
identity = 2;
|
||||||
packages = [ pkgs.claws-mail ];
|
packages = [ pkgs.claws-mail ];
|
||||||
gpu = false;
|
gpu = false;
|
||||||
capability.pulse = false;
|
capability.pulse = false;
|
||||||
}
|
};
|
||||||
{
|
|
||||||
|
"org.weechat" = {
|
||||||
name = "weechat";
|
name = "weechat";
|
||||||
|
identity = 3;
|
||||||
|
shareUid = true;
|
||||||
packages = [ pkgs.weechat ];
|
packages = [ pkgs.weechat ];
|
||||||
capability = {
|
capability = {
|
||||||
wayland = false;
|
wayland = false;
|
||||||
@ -121,10 +127,12 @@ This adds the `environment.fortify` option:
|
|||||||
dbus = true;
|
dbus = true;
|
||||||
pulse = false;
|
pulse = false;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
{
|
|
||||||
|
"dev.vencord.Vesktop" = {
|
||||||
name = "discord";
|
name = "discord";
|
||||||
id = "dev.vencord.Vesktop";
|
identity = 3;
|
||||||
|
shareUid = true;
|
||||||
packages = [ pkgs.vesktop ];
|
packages = [ pkgs.vesktop ];
|
||||||
share = pkgs.vesktop;
|
share = pkgs.vesktop;
|
||||||
command = "vesktop --ozone-platform-hint=wayland";
|
command = "vesktop --ozone-platform-hint=wayland";
|
||||||
@ -142,9 +150,12 @@ This adds the `environment.fortify` option:
|
|||||||
};
|
};
|
||||||
system.filter = true;
|
system.filter = true;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
{
|
|
||||||
|
"io.looking-glass" = {
|
||||||
name = "looking-glass-client";
|
name = "looking-glass-client";
|
||||||
|
identity = 4;
|
||||||
|
useCommonPaths = false;
|
||||||
groups = [ "plugdev" ];
|
groups = [ "plugdev" ];
|
||||||
extraPaths = [
|
extraPaths = [
|
||||||
{
|
{
|
||||||
@ -155,8 +166,8 @@ This adds the `environment.fortify` option:
|
|||||||
extraConfig = {
|
extraConfig = {
|
||||||
programs.looking-glass-client.enable = true;
|
programs.looking-glass-client.enable = true;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
];
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
110
acl/acl-update.c
110
acl/acl-update.c
@ -1,69 +1,71 @@
|
|||||||
#include "acl-update.h"
|
#include "acl-update.h"
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <sys/acl.h>
|
|
||||||
#include <acl/libacl.h>
|
#include <acl/libacl.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/acl.h>
|
||||||
|
|
||||||
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen) {
|
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms,
|
||||||
int ret = -1;
|
size_t plen) {
|
||||||
bool v;
|
int ret = -1;
|
||||||
int i;
|
bool v;
|
||||||
acl_t acl;
|
int i;
|
||||||
acl_entry_t entry;
|
acl_t acl;
|
||||||
acl_tag_t tag_type;
|
acl_entry_t entry;
|
||||||
void *qualifier_p;
|
acl_tag_t tag_type;
|
||||||
acl_permset_t permset;
|
void *qualifier_p;
|
||||||
|
acl_permset_t permset;
|
||||||
|
|
||||||
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
||||||
if (acl == NULL)
|
if (acl == NULL)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
// prune entries by uid
|
// prune entries by uid
|
||||||
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
|
||||||
if (acl_get_tag_type(entry, &tag_type) != 0)
|
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
||||||
return -1;
|
if (acl_get_tag_type(entry, &tag_type) != 0)
|
||||||
if (tag_type != ACL_USER)
|
return -1;
|
||||||
continue;
|
if (tag_type != ACL_USER)
|
||||||
|
continue;
|
||||||
|
|
||||||
qualifier_p = acl_get_qualifier(entry);
|
qualifier_p = acl_get_qualifier(entry);
|
||||||
if (qualifier_p == NULL)
|
if (qualifier_p == NULL)
|
||||||
return -1;
|
return -1;
|
||||||
v = *(uid_t *)qualifier_p == uid;
|
v = *(uid_t *)qualifier_p == uid;
|
||||||
acl_free(qualifier_p);
|
acl_free(qualifier_p);
|
||||||
|
|
||||||
if (!v)
|
if (!v)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
acl_delete_entry(acl, entry);
|
acl_delete_entry(acl, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plen == 0)
|
if (plen == 0)
|
||||||
goto set;
|
goto set;
|
||||||
|
|
||||||
if (acl_create_entry(&acl, &entry) != 0)
|
if (acl_create_entry(&acl, &entry) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
if (acl_get_permset(entry, &permset) != 0)
|
if (acl_get_permset(entry, &permset) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
for (i = 0; i < plen; i++) {
|
for (i = 0; i < plen; i++) {
|
||||||
if (acl_add_perm(permset, perms[i]) != 0)
|
if (acl_add_perm(permset, perms[i]) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
set:
|
set:
|
||||||
if (acl_calc_mask(&acl) != 0)
|
if (acl_calc_mask(&acl) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
if (acl_valid(acl) != 0)
|
if (acl_valid(acl) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
||||||
ret = 0;
|
ret = 0;
|
||||||
|
|
||||||
out:
|
out:
|
||||||
free((void *)path_p);
|
free((void *)path_p);
|
||||||
if (acl != NULL)
|
if (acl != NULL)
|
||||||
acl_free((void *)acl);
|
acl_free((void *)acl);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
#include <sys/acl.h>
|
#include <sys/acl.h>
|
||||||
|
|
||||||
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen);
|
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms,
|
||||||
|
size_t plen);
|
||||||
|
@ -23,7 +23,7 @@ func Update(name string, uid int, perms ...Perm) error {
|
|||||||
p = &perms[0]
|
p = &perms[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := C.f_acl_update_file_by_uid(
|
r, err := C.hakurei_acl_update_file_by_uid(
|
||||||
C.CString(name),
|
C.CString(name),
|
||||||
C.uid_t(uid),
|
C.uid_t(uid),
|
||||||
(*C.acl_perm_t)(p),
|
(*C.acl_perm_t)(p),
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
"git.gensokyo.uk/security/hakurei/acl"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testFileName = "acl.test"
|
const testFileName = "acl.test"
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/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)
|
|
||||||
}
|
|
@ -13,22 +13,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
hsuConfFile = "/etc/hsurc"
|
||||||
fsuConfFile = "/etc/fsurc"
|
envShim = "HAKUREI_SHIM"
|
||||||
envShim = "FORTIFY_SHIM"
|
envAID = "HAKUREI_APP_ID"
|
||||||
envAID = "FORTIFY_APP_ID"
|
envGroups = "HAKUREI_GROUPS"
|
||||||
envGroups = "FORTIFY_GROUPS"
|
|
||||||
|
|
||||||
PR_SET_NO_NEW_PRIVS = 0x26
|
PR_SET_NO_NEW_PRIVS = 0x26
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
Fmain = compPoison
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
log.SetPrefix("fsu: ")
|
log.SetPrefix("hsu: ")
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
|
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
@ -40,20 +35,16 @@ func main() {
|
|||||||
log.Fatal("this program must not be started by root")
|
log.Fatal("this program must not be started by root")
|
||||||
}
|
}
|
||||||
|
|
||||||
var fmain string
|
var toolPath string
|
||||||
if p, ok := checkPath(Fmain); !ok {
|
|
||||||
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
|
|
||||||
} else {
|
|
||||||
fmain = p
|
|
||||||
}
|
|
||||||
|
|
||||||
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
||||||
if p, err := os.Readlink(pexe); err != nil {
|
if p, err := os.Readlink(pexe); err != nil {
|
||||||
log.Fatalf("cannot read parent executable path: %v", err)
|
log.Fatalf("cannot read parent executable path: %v", err)
|
||||||
} else if strings.HasSuffix(p, " (deleted)") {
|
} else if strings.HasSuffix(p, " (deleted)") {
|
||||||
log.Fatal("fortify executable has been deleted")
|
log.Fatal("hakurei executable has been deleted")
|
||||||
} else if p != fmain {
|
} else if p != mustCheckPath(hmain) {
|
||||||
log.Fatal("this program must be started by fortify")
|
log.Fatal("this program must be started by hakurei")
|
||||||
|
} else {
|
||||||
|
toolPath = p
|
||||||
}
|
}
|
||||||
|
|
||||||
// uid = 1000000 +
|
// uid = 1000000 +
|
||||||
@ -61,27 +52,27 @@ func main() {
|
|||||||
// aid
|
// aid
|
||||||
uid := 1000000
|
uid := 1000000
|
||||||
|
|
||||||
// refuse to run if fsurc is not protected correctly
|
// refuse to run if hsurc is not protected correctly
|
||||||
if s, err := os.Stat(fsuConfFile); err != nil {
|
if s, err := os.Stat(hsuConfFile); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
} else if s.Mode().Perm() != 0400 {
|
} else if s.Mode().Perm() != 0400 {
|
||||||
log.Fatal("bad fsurc perm")
|
log.Fatal("bad hsurc perm")
|
||||||
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
|
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
|
||||||
log.Fatal("fsurc must be owned by uid 0")
|
log.Fatal("hsurc must be owned by uid 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate before accepting user input
|
// authenticate before accepting user input
|
||||||
if f, err := os.Open(fsuConfFile); err != nil {
|
if f, err := os.Open(hsuConfFile); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
} else if fid, ok := mustParseConfig(f, puid); !ok {
|
} 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 {
|
} else {
|
||||||
uid += fid * 10000
|
uid += fid * 10000
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowed aid range 0 to 9999
|
// allowed aid range 0 to 9999
|
||||||
if as, ok := os.LookupEnv(envAID); !ok {
|
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 {
|
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
|
||||||
log.Fatal("invalid aid")
|
log.Fatal("invalid aid")
|
||||||
} else {
|
} else {
|
||||||
@ -91,12 +82,12 @@ func main() {
|
|||||||
// pass through setup fd to shim
|
// pass through setup fd to shim
|
||||||
var shimSetupFd string
|
var shimSetupFd string
|
||||||
if s, ok := os.LookupEnv(envShim); !ok {
|
if s, ok := os.LookupEnv(envShim); !ok {
|
||||||
// fortify requests target uid
|
// hakurei requests target uid
|
||||||
// print resolved uid and exit
|
// print resolved uid and exit
|
||||||
fmt.Print(uid)
|
fmt.Print(uid)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
||||||
log.Fatal("FORTIFY_SHIM holds an invalid value")
|
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
||||||
} else {
|
} else {
|
||||||
shimSetupFd = s
|
shimSetupFd = s
|
||||||
}
|
}
|
||||||
@ -133,7 +124,7 @@ func main() {
|
|||||||
panic("uid out of bounds")
|
panic("uid out of bounds")
|
||||||
}
|
}
|
||||||
|
|
||||||
// careful! users in the allowlist is effectively allowed to drop groups via fsu
|
// careful! users in the allowlist is effectively allowed to drop groups via hsu
|
||||||
|
|
||||||
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
||||||
log.Fatalf("cannot set gid: %v", err)
|
log.Fatalf("cannot set gid: %v", err)
|
||||||
@ -147,13 +138,9 @@ func main() {
|
|||||||
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
||||||
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||||
}
|
}
|
||||||
if err := syscall.Exec(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)
|
log.Fatalf("cannot start shim: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
panic("unreachable")
|
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 {
|
if ok {
|
||||||
// allowed fid range 0 to 99
|
// allowed fid range 0 to 99
|
||||||
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
|
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
|
||||||
return -1, false, fmt.Errorf("invalid fortify uid on line %d", line)
|
return -1, false, fmt.Errorf("invalid identity on line %d", line)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
@ -65,7 +65,7 @@ func Test_parseConfig(t *testing.T) {
|
|||||||
{"empty", 0, -1, "", ``},
|
{"empty", 0, -1, "", ``},
|
||||||
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
||||||
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
|
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
|
||||||
{"invalid fid", 1000, -1, "invalid fortify uid on line 1", `1000 f`},
|
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
|
||||||
{"match", 1000, 0, "", `1000 0`},
|
{"match", 1000, 0, "", `1000 0`},
|
||||||
}
|
}
|
||||||
|
|
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"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/dbus"
|
||||||
|
"git.gensokyo.uk/security/hakurei/hst"
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
|
||||||
|
"git.gensokyo.uk/security/hakurei/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.Seccomp |= seccomp.FilterMultiarch
|
||||||
|
}
|
||||||
|
if app.Bluetooth {
|
||||||
|
config.Container.Seccomp |= seccomp.FilterBluetooth
|
||||||
|
}
|
||||||
|
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,
|
lib,
|
||||||
|
stdenv,
|
||||||
|
closureInfo,
|
||||||
writeScript,
|
writeScript,
|
||||||
runtimeShell,
|
runtimeShell,
|
||||||
writeText,
|
writeText,
|
||||||
@ -15,18 +17,21 @@
|
|||||||
runCommand,
|
runCommand,
|
||||||
fetchFromGitHub,
|
fetchFromGitHub,
|
||||||
|
|
||||||
|
zstd,
|
||||||
nix,
|
nix,
|
||||||
|
sqlite,
|
||||||
|
|
||||||
name ? throw "name is required",
|
name ? throw "name is required",
|
||||||
version ? throw "version is required",
|
version ? throw "version is required",
|
||||||
pname ? "${name}-${version}",
|
pname ? "${name}-${version}",
|
||||||
modules ? [ ],
|
modules ? [ ],
|
||||||
|
nixosModules ? [ ],
|
||||||
script ? ''
|
script ? ''
|
||||||
exec "$SHELL" "$@"
|
exec "$SHELL" "$@"
|
||||||
'',
|
'',
|
||||||
|
|
||||||
id ? name,
|
id ? name,
|
||||||
app_id ? throw "app_id is required",
|
identity ? throw "identity is required",
|
||||||
groups ? [ ],
|
groups ? [ ],
|
||||||
userns ? false,
|
userns ? false,
|
||||||
net ? true,
|
net ? true,
|
||||||
@ -52,7 +57,7 @@ let
|
|||||||
modules = modules ++ [
|
modules = modules ++ [
|
||||||
{
|
{
|
||||||
home = {
|
home = {
|
||||||
username = "fortify";
|
username = "hakurei";
|
||||||
homeDirectory = "/data/data/${id}";
|
homeDirectory = "/data/data/${id}";
|
||||||
stateVersion = "22.11";
|
stateVersion = "22.11";
|
||||||
};
|
};
|
||||||
@ -60,7 +65,7 @@ let
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
launcher = writeScript "fortify-${pname}" ''
|
launcher = writeScript "hakurei-${pname}" ''
|
||||||
#!${runtimeShell} -el
|
#!${runtimeShell} -el
|
||||||
${script}
|
${script}
|
||||||
'';
|
'';
|
||||||
@ -72,6 +77,8 @@ let
|
|||||||
etc.nixpkgs.source = nixpkgs.outPath;
|
etc.nixpkgs.source = nixpkgs.outPath;
|
||||||
systemPackages = [ pkgs.nix ];
|
systemPackages = [ pkgs.nix ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
imports = nixosModules;
|
||||||
};
|
};
|
||||||
nixos = nixpkgs.lib.nixosSystem {
|
nixos = nixpkgs.lib.nixosSystem {
|
||||||
inherit system;
|
inherit system;
|
||||||
@ -140,7 +147,7 @@ let
|
|||||||
name
|
name
|
||||||
version
|
version
|
||||||
id
|
id
|
||||||
app_id
|
identity
|
||||||
launcher
|
launcher
|
||||||
groups
|
groups
|
||||||
userns
|
userns
|
||||||
@ -164,11 +171,7 @@ let
|
|||||||
broadcast = { };
|
broadcast = { };
|
||||||
});
|
});
|
||||||
|
|
||||||
enablements =
|
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);
|
||||||
(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;
|
mesa = if gpu then mesaWrappers else null;
|
||||||
nix_gl = if gpu then nixGL else null;
|
nix_gl = if gpu then nixGL else null;
|
||||||
@ -177,26 +180,73 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
||||||
writeScript "fortify-${pname}-bundle-prelude" ''
|
stdenv.mkDerivation {
|
||||||
#!${runtimeShell} -el
|
name = "${pname}.pkg";
|
||||||
OUT="$(mktemp -d)"
|
inherit version;
|
||||||
TAR="$(mktemp -u)"
|
__structuredAttrs = true;
|
||||||
set -x
|
|
||||||
|
|
||||||
nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
|
nativeBuildInputs = [
|
||||||
nix store --store "$OUT" optimise
|
zstd
|
||||||
chmod -R +r "$OUT/nix/var"
|
nix
|
||||||
nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19¶llel-compression=true" \
|
sqlite
|
||||||
"${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"
|
|
||||||
|
|
||||||
# creating an intermediate file improves zstd performance
|
buildCommand = ''
|
||||||
tar -C "$OUT" -cf "$TAR" .
|
NIX_ROOT="$(mktemp -d)"
|
||||||
chmod +w -R "$OUT" && rm -rf "$OUT"
|
export USER="nobody"
|
||||||
|
|
||||||
zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
|
# create bootstrap store
|
||||||
rm "$TAR"
|
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"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/command"
|
||||||
|
"git.gensokyo.uk/security/hakurei/hst"
|
||||||
|
"git.gensokyo.uk/security/hakurei/internal"
|
||||||
|
"git.gensokyo.uk/security/hakurei/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.InstallFmsg(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"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/hst"
|
||||||
|
"git.gensokyo.uk/security/hakurei/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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
@ -8,33 +9,27 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/hakurei/hst"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/hakurei/internal"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/hakurei/internal/hlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
var hakureiPath = internal.MustHakureiPath()
|
||||||
|
|
||||||
var (
|
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
|
||||||
Fmain = compPoison
|
|
||||||
)
|
|
||||||
|
|
||||||
func fortifyApp(config *fst.Config, beforeFail func()) {
|
|
||||||
var (
|
var (
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
st io.WriteCloser
|
st io.WriteCloser
|
||||||
)
|
)
|
||||||
if p, ok := internal.Path(Fmain); !ok {
|
|
||||||
beforeFail()
|
if r, w, err := os.Pipe(); err != nil {
|
||||||
log.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
|
|
||||||
} else if r, w, err := os.Pipe(); err != nil {
|
|
||||||
beforeFail()
|
beforeFail()
|
||||||
log.Fatalf("cannot pipe: %v", err)
|
log.Fatalf("cannot pipe: %v", err)
|
||||||
} else {
|
} else {
|
||||||
if fmsg.Load() {
|
if hlog.Load() {
|
||||||
cmd = exec.Command(p, "-v", "app", "3")
|
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
|
||||||
} else {
|
} 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.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
cmd.ExtraFiles = []*os.File{r}
|
cmd.ExtraFiles = []*os.File{r}
|
||||||
@ -50,7 +45,7 @@ func fortifyApp(config *fst.Config, beforeFail func()) {
|
|||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
beforeFail()
|
beforeFail()
|
||||||
log.Fatalf("cannot start fortify: %v", err)
|
log.Fatalf("cannot start hakurei: %v", err)
|
||||||
}
|
}
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
var exitError *exec.ExitError
|
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"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/hst"
|
||||||
|
"git.gensokyo.uk/security/hakurei/internal"
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
Seccomp: seccomp.FilterMultiarch,
|
||||||
|
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,
|
||||||
|
Seccomp: seccomp.FilterMultiarch,
|
||||||
|
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"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/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"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/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'
|
|
@ -5,7 +5,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/hakurei/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
|
@ -5,8 +5,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ProxyPair is an upstream dbus address and a downstream socket path.
|
||||||
|
type ProxyPair [2]string
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// See set 'see' policy for NAME (--see=NAME)
|
// See set 'see' policy for NAME (--see=NAME)
|
||||||
See []string `json:"see"`
|
See []string `json:"see"`
|
||||||
@ -24,7 +28,62 @@ type Config struct {
|
|||||||
Filter bool `json:"filter"`
|
Filter bool `json:"filter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Args(bus [2]string) (args []string) {
|
func (c *Config) interfaces(yield func(string) bool) {
|
||||||
|
for _, iface := range c.See {
|
||||||
|
if !yield(iface) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, iface := range c.Talk {
|
||||||
|
if !yield(iface) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, iface := range c.Own {
|
||||||
|
if !yield(iface) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for iface := range c.Call {
|
||||||
|
if !yield(iface) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for iface := range c.Broadcast {
|
||||||
|
if !yield(iface) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) checkInterfaces(segment string) error {
|
||||||
|
for iface := range c.interfaces {
|
||||||
|
/*
|
||||||
|
xdg-dbus-proxy fails without output when this condition is not met:
|
||||||
|
char *dot = strrchr (filter->interface, '.');
|
||||||
|
if (dot != NULL)
|
||||||
|
{
|
||||||
|
*dot = 0;
|
||||||
|
if (strcmp (dot + 1, "*") != 0)
|
||||||
|
filter->member = g_strdup (dot + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
trim ".*" since they are removed before searching for '.':
|
||||||
|
if (g_str_has_suffix (name, ".*"))
|
||||||
|
{
|
||||||
|
name[strlen (name) - 2] = 0;
|
||||||
|
wildcard = TRUE;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if strings.IndexByte(strings.TrimSuffix(iface, ".*"), '.') == -1 {
|
||||||
|
return &BadInterfaceError{iface, segment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Args(bus ProxyPair) (args []string) {
|
||||||
argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast)
|
argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast)
|
||||||
if c.Log {
|
if c.Log {
|
||||||
argc++
|
argc++
|
||||||
@ -60,9 +119,7 @@ func (c *Config) Args(bus [2]string) (args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Load(r io.Reader) error {
|
func (c *Config) Load(r io.Reader) error { return json.NewDecoder(r).Decode(&c) }
|
||||||
return json.NewDecoder(r).Decode(&c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConfigFromFile opens the target config file at path and parses its contents into *Config.
|
// NewConfigFromFile opens the target config file at path and parses its contents into *Config.
|
||||||
func NewConfigFromFile(path string) (*Config, error) {
|
func NewConfigFromFile(path string) (*Config, error) {
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/hakurei/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_Args(t *testing.T) {
|
func TestConfig_Args(t *testing.T) {
|
||||||
|
@ -1,74 +1,40 @@
|
|||||||
package dbus_test
|
package dbus_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/hakurei/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
|
"git.gensokyo.uk/security/hakurei/internal"
|
||||||
|
"git.gensokyo.uk/security/hakurei/internal/hlog"
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestFinalise(t *testing.T) {
|
||||||
for _, tc := range [][2][2]string{
|
if _, err := dbus.Finalise(dbus.ProxyPair{}, dbus.ProxyPair{}, nil, nil); !errors.Is(err, syscall.EBADE) {
|
||||||
{
|
t.Errorf("Finalise: error = %v, want %v",
|
||||||
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/1ca5d183ef4c99e74c3e544715f32702/bus"},
|
err, syscall.EBADE)
|
||||||
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/1ca5d183ef4c99e74c3e544715f32702/system_bus_socket"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/881ac3796ff3f3bf0a773824383187a0/bus"},
|
|
||||||
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/881ac3796ff3f3bf0a773824383187a0/system_bus_socket"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/3d1a5084520ef79c0c6a49a675bac701/bus"},
|
|
||||||
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/3d1a5084520ef79c0c6a49a675bac701/system_bus_socket"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/2a1639bab712799788ea0ff7aa280c35/bus"},
|
|
||||||
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/2a1639bab712799788ea0ff7aa280c35/system_bus_socket"},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run("create instance for "+tc[0][0]+" and "+tc[1][0], func(t *testing.T) {
|
|
||||||
if got := dbus.New(tc[0], tc[1]); !got.CompareTestNew(tc[0], tc[1]) {
|
|
||||||
t.Errorf("New(%q, %q) = %v",
|
|
||||||
tc[0], tc[1],
|
|
||||||
got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProxy_Seal(t *testing.T) {
|
|
||||||
t.Run("double seal panic", func(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
want := "dbus proxy sealed twice"
|
|
||||||
if r := recover(); r != want {
|
|
||||||
t.Errorf("Seal: panic = %q, want %q",
|
|
||||||
r, want)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
p := dbus.New([2]string{}, [2]string{})
|
|
||||||
_ = p.Seal(dbus.NewConfig("", true, false), nil)
|
|
||||||
_ = p.Seal(dbus.NewConfig("", true, false), nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
ep := dbus.New([2]string{}, [2]string{})
|
|
||||||
if err := ep.Seal(nil, nil); !errors.Is(err, dbus.ErrConfig) {
|
|
||||||
t.Errorf("Seal(nil, nil) error = %v, want %v",
|
|
||||||
err, dbus.ErrConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, tc := range testCasePairs() {
|
for id, tc := range testCasePairs() {
|
||||||
t.Run("create seal for "+id, func(t *testing.T) {
|
t.Run("create final for "+id, func(t *testing.T) {
|
||||||
p := dbus.New(tc[0].bus, tc[1].bus)
|
var wt io.WriterTo
|
||||||
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, helper.ErrContainsNull)) != tc[0].wantErr {
|
if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
|
||||||
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
|
t.Errorf("Finalise: error = %v, wantErr %v",
|
||||||
tc[0].c, tc[1].c,
|
|
||||||
err, tc[0].wantErr)
|
err, tc[0].wantErr)
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
wt = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// rest of the tests happen for sealed instances
|
// rest of the tests happen for sealed instances
|
||||||
@ -81,136 +47,167 @@ func TestProxy_Seal(t *testing.T) {
|
|||||||
args := append(tc[0].want, tc[1].want...)
|
args := append(tc[0].want, tc[1].want...)
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
want.WriteString(arg)
|
want.WriteString(arg)
|
||||||
want.WriteByte('\x00')
|
want.WriteByte(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
wt := p.AccessTestProxySeal()
|
|
||||||
got := new(strings.Builder)
|
got := new(strings.Builder)
|
||||||
if _, err := wt.WriteTo(got); err != nil {
|
if _, err := wt.WriteTo(got); err != nil {
|
||||||
t.Errorf("p.seal.WriteTo(): %v", err)
|
t.Errorf("WriteTo: error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if want.String() != got.String() {
|
if want.String() != got.String() {
|
||||||
t.Errorf("Seal(%p, %p) seal = %v, want %v",
|
t.Errorf("Seal: %q, want %q",
|
||||||
tc[0].c, tc[1].c,
|
|
||||||
got.String(), want.String())
|
got.String(), want.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProxy_Start_Wait_Close_String(t *testing.T) {
|
func TestProxyStartWaitCloseString(t *testing.T) {
|
||||||
t.Run("sandboxed", func(t *testing.T) {
|
oldWaitDelay := helper.WaitDelay
|
||||||
testProxyStartWaitCloseString(t, true)
|
helper.WaitDelay = 16 * time.Second
|
||||||
})
|
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
||||||
t.Run("direct", func(t *testing.T) {
|
|
||||||
testProxyStartWaitCloseString(t, false)
|
t.Run("sandbox", func(t *testing.T) {
|
||||||
|
proxyName := dbus.ProxyName
|
||||||
|
dbus.ProxyName = os.Args[0]
|
||||||
|
t.Cleanup(func() { dbus.ProxyName = proxyName })
|
||||||
|
testProxyFinaliseStartWaitCloseString(t, true)
|
||||||
})
|
})
|
||||||
|
t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
|
||||||
|
var p *dbus.Proxy
|
||||||
|
|
||||||
|
t.Run("string for nil proxy", func(t *testing.T) {
|
||||||
|
want := "(invalid dbus proxy)"
|
||||||
|
if got := p.String(); got != want {
|
||||||
|
t.Errorf("String: %q, want %q",
|
||||||
|
got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid start", func(t *testing.T) {
|
||||||
|
if !useSandbox {
|
||||||
|
p = dbus.NewDirect(t.Context(), nil, nil)
|
||||||
|
} else {
|
||||||
|
p = dbus.New(t.Context(), nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) {
|
||||||
|
t.Errorf("Start: error = %q, wantErr %q",
|
||||||
|
err, syscall.ENOTRECOVERABLE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for id, tc := range testCasePairs() {
|
for id, tc := range testCasePairs() {
|
||||||
// this test does not test errors
|
// this test does not test errors
|
||||||
if tc[0].wantErr {
|
if tc[0].wantErr {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("string for nil proxy", func(t *testing.T) {
|
|
||||||
var p *dbus.Proxy
|
|
||||||
want := "(invalid dbus proxy)"
|
|
||||||
if got := p.String(); got != want {
|
|
||||||
t.Errorf("String() = %v, want %v",
|
|
||||||
got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("proxy for "+id, func(t *testing.T) {
|
t.Run("proxy for "+id, func(t *testing.T) {
|
||||||
helper.InternalReplaceExecCommand(t)
|
var final *dbus.Final
|
||||||
overridePath(t)
|
t.Run("finalise", func(t *testing.T) {
|
||||||
|
if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); err != nil {
|
||||||
p := dbus.New(tc[0].bus, tc[1].bus)
|
t.Errorf("Finalise: error = %v, wantErr %v",
|
||||||
output := new(strings.Builder)
|
|
||||||
|
|
||||||
t.Run("unsealed behaviour of "+id, func(t *testing.T) {
|
|
||||||
t.Run("unsealed string of "+id, func(t *testing.T) {
|
|
||||||
want := "(unsealed dbus proxy)"
|
|
||||||
if got := p.String(); got != want {
|
|
||||||
t.Errorf("String() = %v, want %v",
|
|
||||||
got, want)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("unsealed start of "+id, func(t *testing.T) {
|
|
||||||
want := "proxy not sealed"
|
|
||||||
if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
|
|
||||||
t.Errorf("Start() error = %v, wantErr %q",
|
|
||||||
err, errors.New(want))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("unsealed wait of "+id, func(t *testing.T) {
|
|
||||||
wantErr := "dbus: not started"
|
|
||||||
if err := p.Wait(); err == nil || err.Error() != wantErr {
|
|
||||||
t.Errorf("Wait() error = %v, wantErr %v",
|
|
||||||
err, wantErr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("seal with "+id, func(t *testing.T) {
|
|
||||||
if err := p.Seal(tc[0].c, tc[1].c); err != nil {
|
|
||||||
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
|
|
||||||
tc[0].c, tc[1].c,
|
|
||||||
err, tc[0].wantErr)
|
err, tc[0].wantErr)
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
final = v
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("sealed behaviour of "+id, func(t *testing.T) {
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
|
defer cancel()
|
||||||
|
if !useSandbox {
|
||||||
|
p = dbus.NewDirect(ctx, final, nil)
|
||||||
|
} else {
|
||||||
|
p = dbus.New(ctx, final, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
|
||||||
|
return exec.CommandContext(ctx, os.Args[0], "-test.v",
|
||||||
|
"-test.run=TestHelperInit", "--", "init")
|
||||||
|
}
|
||||||
|
p.CmdF = func(v any) {
|
||||||
|
if useSandbox {
|
||||||
|
container := v.(*sandbox.Container)
|
||||||
|
if container.Args[0] != dbus.ProxyName {
|
||||||
|
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
|
||||||
|
}
|
||||||
|
container.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, container.Args[1:]...)
|
||||||
|
} else {
|
||||||
|
cmd := v.(*exec.Cmd)
|
||||||
|
if cmd.Args[0] != dbus.ProxyName {
|
||||||
|
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
|
||||||
|
}
|
||||||
|
cmd.Err = nil
|
||||||
|
cmd.Path = os.Args[0]
|
||||||
|
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
|
||||||
|
output := new(strings.Builder)
|
||||||
|
|
||||||
|
t.Run("invalid wait", func(t *testing.T) {
|
||||||
|
wantErr := "dbus: not started"
|
||||||
|
if err := p.Wait(); err == nil || err.Error() != wantErr {
|
||||||
|
t.Errorf("Wait: error = %v, wantErr %v",
|
||||||
|
err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
want := "(unused dbus proxy)"
|
||||||
if got := p.String(); got != want {
|
if got := p.String(); got != want {
|
||||||
t.Errorf("String() = %v, want %v",
|
t.Errorf("String: %q, want %q",
|
||||||
got, want)
|
got, want)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("sealed start of "+id, func(t *testing.T) {
|
t.Run("start", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
if err := p.Start(); err != nil {
|
||||||
defer cancel()
|
t.Fatalf("Start: error = %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.Start(ctx, output, sandbox); err != nil {
|
t.Run("string", func(t *testing.T) {
|
||||||
t.Fatalf("Start(nil, nil) error = %v",
|
wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
|
||||||
err)
|
if useSandbox {
|
||||||
|
wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], flags: 0x0, seccomp: 0x3e`, os.Args[0])
|
||||||
}
|
}
|
||||||
|
if got := p.String(); !strings.Contains(got, wantSubstr) {
|
||||||
|
t.Errorf("String: %q, want %q",
|
||||||
|
got, wantSubstr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("started string of "+id, func(t *testing.T) {
|
t.Run("wait", func(t *testing.T) {
|
||||||
wantSubstr := dbus.ProxyName + " --args="
|
done := make(chan struct{})
|
||||||
if got := p.String(); !strings.Contains(got, wantSubstr) {
|
go func() {
|
||||||
t.Errorf("String() = %v, want %v",
|
|
||||||
p.String(), wantSubstr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("started wait of "+id, func(t *testing.T) {
|
|
||||||
p.Close()
|
|
||||||
if err := p.Wait(); err != nil {
|
if err := p.Wait(); err != nil {
|
||||||
t.Errorf("Wait() error = %v\noutput: %s",
|
t.Errorf("Wait: error = %v\noutput: %s",
|
||||||
err, output.String())
|
err, output.String())
|
||||||
}
|
}
|
||||||
})
|
close(done)
|
||||||
|
}()
|
||||||
|
p.Close()
|
||||||
|
<-done
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func overridePath(t *testing.T) {
|
func TestHelperInit(t *testing.T) {
|
||||||
proxyName := dbus.ProxyName
|
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||||
dbus.ProxyName = "/nonexistent-xdg-dbus-proxy"
|
return
|
||||||
t.Cleanup(func() {
|
}
|
||||||
dbus.ProxyName = proxyName
|
sandbox.SetOutput(hlog.Output{})
|
||||||
})
|
sandbox.Init(hlog.Prepare, internal.InstallFmsg)
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package dbus
|
package dbus
|
||||||
|
|
||||||
import "io"
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
// CompareTestNew provides TestNew with comparison access to unexported Proxy fields.
|
// NewDirect returns a new instance of [Proxy] with its sandbox disabled.
|
||||||
func (p *Proxy) CompareTestNew(session, system [2]string) bool {
|
func NewDirect(ctx context.Context, final *Final, output io.Writer) *Proxy {
|
||||||
return session == p.session && system == p.system
|
p := New(ctx, final, output)
|
||||||
}
|
p.useSandbox = false
|
||||||
|
return p
|
||||||
// AccessTestProxySeal provides TestProxy_Seal with access to unexported Proxy seal field.
|
|
||||||
func (p *Proxy) AccessTestProxySeal() io.WriterTo {
|
|
||||||
return p.seal
|
|
||||||
}
|
}
|
||||||
|
188
dbus/proc.go
Normal file
188
dbus/proc.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
package dbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
|
"git.gensokyo.uk/security/hakurei/ldd"
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox"
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start starts and configures a D-Bus proxy process.
|
||||||
|
func (p *Proxy) Start() error {
|
||||||
|
if p.final == nil || p.final.WriterTo == nil {
|
||||||
|
return syscall.ENOTRECOVERABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.pmu.Lock()
|
||||||
|
defer p.pmu.Unlock()
|
||||||
|
|
||||||
|
if p.cancel != nil || p.cause != nil {
|
||||||
|
return errors.New("dbus: already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancelCause(p.ctx)
|
||||||
|
|
||||||
|
if !p.useSandbox {
|
||||||
|
p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) {
|
||||||
|
if p.CmdF != nil {
|
||||||
|
p.CmdF(cmd)
|
||||||
|
}
|
||||||
|
if p.output != nil {
|
||||||
|
cmd.Stdout, cmd.Stderr = p.output, p.output
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
cmd.Env = make([]string, 0)
|
||||||
|
}, nil)
|
||||||
|
} else {
|
||||||
|
toolPath := p.name
|
||||||
|
if filepath.Base(p.name) == p.name {
|
||||||
|
if s, err := exec.LookPath(p.name); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
toolPath = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var libPaths []string
|
||||||
|
if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
libPaths = ldd.Path(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.helper = helper.New(
|
||||||
|
ctx, toolPath,
|
||||||
|
p.final, true,
|
||||||
|
argF, func(container *sandbox.Container) {
|
||||||
|
container.Seccomp |= seccomp.FilterMultiarch
|
||||||
|
container.Hostname = "hakurei-dbus"
|
||||||
|
container.CommandContext = p.CommandContext
|
||||||
|
if p.output != nil {
|
||||||
|
container.Stdout, container.Stderr = p.output, p.output
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.CmdF != nil {
|
||||||
|
p.CmdF(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// these lib paths are unpredictable, so mount them first so they cannot cover anything
|
||||||
|
for _, name := range libPaths {
|
||||||
|
container.Bind(name, name, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// upstream bus directories
|
||||||
|
upstreamPaths := make([]string, 0, 2)
|
||||||
|
for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} {
|
||||||
|
for _, ent := range addr {
|
||||||
|
if ent.Method != "unix" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, pair := range ent.Values {
|
||||||
|
if pair[0] != "path" || !path.IsAbs(pair[1]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
upstreamPaths = append(upstreamPaths, path.Dir(pair[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(upstreamPaths)
|
||||||
|
upstreamPaths = slices.Compact(upstreamPaths)
|
||||||
|
for _, name := range upstreamPaths {
|
||||||
|
container.Bind(name, name, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent directories of bind paths
|
||||||
|
sockDirPaths := make([]string, 0, 2)
|
||||||
|
if d := path.Dir(p.final.Session[1]); path.IsAbs(d) {
|
||||||
|
sockDirPaths = append(sockDirPaths, d)
|
||||||
|
}
|
||||||
|
if d := path.Dir(p.final.System[1]); path.IsAbs(d) {
|
||||||
|
sockDirPaths = append(sockDirPaths, d)
|
||||||
|
}
|
||||||
|
slices.Sort(sockDirPaths)
|
||||||
|
sockDirPaths = slices.Compact(sockDirPaths)
|
||||||
|
for _, name := range sockDirPaths {
|
||||||
|
container.Bind(name, name, sandbox.BindWritable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// xdg-dbus-proxy bin path
|
||||||
|
binPath := path.Dir(toolPath)
|
||||||
|
container.Bind(binPath, binPath, 0)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.helper.Start(); err != nil {
|
||||||
|
cancel(err)
|
||||||
|
p.helper = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cancel, p.cause = cancel, func() error { return context.Cause(ctx) }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyClosed = errors.New("proxy closed")
|
||||||
|
|
||||||
|
// Wait blocks until xdg-dbus-proxy exits and releases resources.
|
||||||
|
func (p *Proxy) Wait() error {
|
||||||
|
p.mu.RLock()
|
||||||
|
defer p.mu.RUnlock()
|
||||||
|
|
||||||
|
p.pmu.RLock()
|
||||||
|
if p.helper == nil || p.cancel == nil || p.cause == nil {
|
||||||
|
p.pmu.RUnlock()
|
||||||
|
return errors.New("dbus: not started")
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := make([]error, 3)
|
||||||
|
|
||||||
|
errs[0] = p.helper.Wait()
|
||||||
|
if errors.Is(errs[0], context.Canceled) &&
|
||||||
|
errors.Is(p.cause(), proxyClosed) {
|
||||||
|
errs[0] = nil
|
||||||
|
}
|
||||||
|
p.pmu.RUnlock()
|
||||||
|
|
||||||
|
// ensure socket removal so ephemeral directory is empty at revert
|
||||||
|
if err := os.Remove(p.final.Session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
errs[1] = err
|
||||||
|
}
|
||||||
|
if p.final.System[1] != "" {
|
||||||
|
if err := os.Remove(p.final.System[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
errs[2] = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
|
||||||
|
func (p *Proxy) Close() {
|
||||||
|
p.pmu.Lock()
|
||||||
|
defer p.pmu.Unlock()
|
||||||
|
|
||||||
|
if p.cancel == nil {
|
||||||
|
panic("dbus: not started")
|
||||||
|
}
|
||||||
|
p.cancel(proxyClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func argF(argsFd, statFd int) []string {
|
||||||
|
if statFd == -1 {
|
||||||
|
return []string{"--args=" + strconv.Itoa(argsFd)}
|
||||||
|
} else {
|
||||||
|
return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
|
||||||
|
}
|
||||||
|
}
|
126
dbus/proxy.go
126
dbus/proxy.go
@ -2,94 +2,116 @@ package dbus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProxyName is the file name or path to the proxy program.
|
// ProxyName is the file name or path to the proxy program.
|
||||||
// Overriding ProxyName will only affect Proxy instance created after the change.
|
// Overriding ProxyName will only affect Proxy instance created after the change.
|
||||||
var ProxyName = "xdg-dbus-proxy"
|
var ProxyName = "xdg-dbus-proxy"
|
||||||
|
|
||||||
// Proxy holds references to a xdg-dbus-proxy process, and should never be copied.
|
type BadInterfaceError struct {
|
||||||
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
|
Interface string
|
||||||
type Proxy struct {
|
Segment string
|
||||||
helper helper.Helper
|
|
||||||
bwrap *bwrap.Config
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelCauseFunc
|
|
||||||
|
|
||||||
name string
|
|
||||||
session [2]string
|
|
||||||
system [2]string
|
|
||||||
sysP bool
|
|
||||||
|
|
||||||
seal io.WriterTo
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Proxy) Session() [2]string { return p.session }
|
func (e *BadInterfaceError) Error() string {
|
||||||
func (p *Proxy) System() [2]string { return p.system }
|
return fmt.Sprintf("bad interface string %q in %s bus configuration", e.Interface, e.Segment)
|
||||||
func (p *Proxy) Sealed() bool { p.lock.RLock(); defer p.lock.RUnlock(); return p.seal != nil }
|
}
|
||||||
|
|
||||||
var (
|
// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied.
|
||||||
ErrConfig = errors.New("no configuration to seal")
|
type Proxy struct {
|
||||||
)
|
helper helper.Helper
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
cancel context.CancelCauseFunc
|
||||||
|
cause func() error
|
||||||
|
|
||||||
|
final *Final
|
||||||
|
output io.Writer
|
||||||
|
useSandbox bool
|
||||||
|
|
||||||
|
name string
|
||||||
|
CmdF func(any)
|
||||||
|
|
||||||
|
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
|
||||||
|
FilterF func([]byte) []byte
|
||||||
|
|
||||||
|
mu, pmu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Proxy) String() string {
|
func (p *Proxy) String() string {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return "(invalid dbus proxy)"
|
return "(invalid dbus proxy)"
|
||||||
}
|
}
|
||||||
|
|
||||||
p.lock.RLock()
|
p.mu.RLock()
|
||||||
defer p.lock.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
|
|
||||||
if p.helper != nil {
|
if p.helper != nil {
|
||||||
return p.helper.String()
|
return p.helper.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.seal != nil {
|
return "(unused dbus proxy)"
|
||||||
return p.seal.(fmt.Stringer).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return "(unsealed dbus proxy)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seal seals the Proxy instance.
|
// Final describes the outcome of a proxy configuration.
|
||||||
func (p *Proxy) Seal(session, system *Config) error {
|
type Final struct {
|
||||||
p.lock.Lock()
|
Session, System ProxyPair
|
||||||
defer p.lock.Unlock()
|
// parsed upstream address
|
||||||
|
SessionUpstream, SystemUpstream []AddrEntry
|
||||||
if p.seal != nil {
|
io.WriterTo
|
||||||
panic("dbus proxy sealed twice")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// Finalise creates a checked argument writer for [Proxy].
|
||||||
|
func Finalise(sessionBus, systemBus ProxyPair, session, system *Config) (final *Final, err error) {
|
||||||
if session == nil && system == nil {
|
if session == nil && system == nil {
|
||||||
return ErrConfig
|
return nil, syscall.EBADE
|
||||||
}
|
}
|
||||||
|
|
||||||
var args []string
|
var args []string
|
||||||
if session != nil {
|
if session != nil {
|
||||||
args = append(args, session.Args(p.session)...)
|
if err = session.checkInterfaces("session"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
args = append(args, session.Args(sessionBus)...)
|
||||||
}
|
}
|
||||||
if system != nil {
|
if system != nil {
|
||||||
args = append(args, system.Args(p.system)...)
|
if err = system.checkInterfaces("system"); err != nil {
|
||||||
p.sysP = true
|
return
|
||||||
}
|
}
|
||||||
if seal, err := helper.NewCheckedArgs(args); err != nil {
|
args = append(args, system.Args(systemBus)...)
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
p.seal = seal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
final = &Final{Session: sessionBus, System: systemBus}
|
||||||
|
|
||||||
|
final.WriterTo, err = helper.NewCheckedArgs(args)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if session != nil {
|
||||||
|
final.SessionUpstream, err = Parse([]byte(final.Session[0]))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if system != nil {
|
||||||
|
final.SystemUpstream, err = Parse([]byte(final.System[0]))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a reference to a new unsealed Proxy.
|
// New returns a new instance of [Proxy].
|
||||||
func New(session, system [2]string) *Proxy {
|
func New(ctx context.Context, final *Final, output io.Writer) *Proxy {
|
||||||
return &Proxy{name: ProxyName, session: session, system: system}
|
return &Proxy{name: ProxyName, ctx: ctx, final: final, output: output, useSandbox: true}
|
||||||
}
|
}
|
||||||
|
175
dbus/run.go
175
dbus/run.go
@ -1,175 +0,0 @@
|
|||||||
package dbus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
"git.gensokyo.uk/security/fortify/ldd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start launches the D-Bus proxy.
|
|
||||||
func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error {
|
|
||||||
p.lock.Lock()
|
|
||||||
defer p.lock.Unlock()
|
|
||||||
|
|
||||||
if p.seal == nil {
|
|
||||||
return errors.New("proxy not sealed")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
h helper.Helper
|
|
||||||
|
|
||||||
argF = func(argsFD, statFD int) []string {
|
|
||||||
if statFD == -1 {
|
|
||||||
return []string{"--args=" + strconv.Itoa(argsFD)}
|
|
||||||
} else {
|
|
||||||
return []string{"--args=" + strconv.Itoa(argsFD), "--fd=" + strconv.Itoa(statFD)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if !sandbox {
|
|
||||||
h = helper.New(p.seal, p.name, argF)
|
|
||||||
// xdg-dbus-proxy does not need to inherit the environment
|
|
||||||
h.SetEnv(make([]string, 0))
|
|
||||||
} else {
|
|
||||||
// look up absolute path if name is just a file name
|
|
||||||
toolPath := p.name
|
|
||||||
if filepath.Base(p.name) == p.name {
|
|
||||||
if s, err := exec.LookPath(p.name); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
toolPath = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve libraries by parsing ldd output
|
|
||||||
var proxyDeps []*ldd.Entry
|
|
||||||
if toolPath != "/nonexistent-xdg-dbus-proxy" {
|
|
||||||
if l, err := ldd.Exec(ctx, toolPath); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
proxyDeps = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bc := &bwrap.Config{
|
|
||||||
Unshare: nil,
|
|
||||||
Hostname: "fortify-dbus",
|
|
||||||
Chdir: "/",
|
|
||||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
|
||||||
Clearenv: true,
|
|
||||||
NewSession: true,
|
|
||||||
DieWithParent: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve proxy socket directories
|
|
||||||
bindTarget := make(map[string]struct{}, 2)
|
|
||||||
for _, ps := range []string{p.session[1], p.system[1]} {
|
|
||||||
if pd := path.Dir(ps); len(pd) > 0 {
|
|
||||||
if pd[0] == '/' {
|
|
||||||
bindTarget[pd] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k := range bindTarget {
|
|
||||||
bc.Bind(k, k, false, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
roBindTarget := make(map[string]struct{}, 2+1+len(proxyDeps))
|
|
||||||
|
|
||||||
// xdb-dbus-proxy bin and dependencies
|
|
||||||
roBindTarget[path.Dir(toolPath)] = struct{}{}
|
|
||||||
for _, ent := range proxyDeps {
|
|
||||||
if path.IsAbs(ent.Path) {
|
|
||||||
roBindTarget[path.Dir(ent.Path)] = struct{}{}
|
|
||||||
}
|
|
||||||
if path.IsAbs(ent.Name) {
|
|
||||||
roBindTarget[path.Dir(ent.Name)] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve upstream bus directories
|
|
||||||
for _, as := range []string{p.session[0], p.system[0]} {
|
|
||||||
if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
|
|
||||||
// leave / intact
|
|
||||||
roBindTarget[path.Dir(as[10:])] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for k := range roBindTarget {
|
|
||||||
bc.Bind(k, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
|
|
||||||
p.bwrap = bc
|
|
||||||
}
|
|
||||||
|
|
||||||
if output != nil {
|
|
||||||
h.Stdout(output).Stderr(output)
|
|
||||||
}
|
|
||||||
c, cancel := context.WithCancelCause(ctx)
|
|
||||||
if err := h.Start(c, true); err != nil {
|
|
||||||
cancel(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.helper = h
|
|
||||||
p.ctx = c
|
|
||||||
p.cancel = cancel
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxyClosed = errors.New("proxy closed")
|
|
||||||
|
|
||||||
// Wait blocks until xdg-dbus-proxy exits and releases resources.
|
|
||||||
func (p *Proxy) Wait() error {
|
|
||||||
p.lock.RLock()
|
|
||||||
defer p.lock.RUnlock()
|
|
||||||
|
|
||||||
if p.helper == nil {
|
|
||||||
return errors.New("dbus: not started")
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make([]error, 3)
|
|
||||||
|
|
||||||
errs[0] = p.helper.Wait()
|
|
||||||
if p.cancel == nil &&
|
|
||||||
errors.Is(errs[0], context.Canceled) &&
|
|
||||||
errors.Is(context.Cause(p.ctx), proxyClosed) {
|
|
||||||
errs[0] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure socket removal so ephemeral directory is empty at revert
|
|
||||||
if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
errs[1] = err
|
|
||||||
}
|
|
||||||
if p.sysP {
|
|
||||||
if err := os.Remove(p.system[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
errs[2] = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Join(errs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
|
|
||||||
func (p *Proxy) Close() {
|
|
||||||
p.lock.Lock()
|
|
||||||
defer p.lock.Unlock()
|
|
||||||
|
|
||||||
if p.cancel == nil {
|
|
||||||
panic("dbus: not started")
|
|
||||||
}
|
|
||||||
p.cancel(proxyClosed)
|
|
||||||
p.cancel = nil
|
|
||||||
}
|
|
@ -3,7 +3,13 @@ package dbus_test
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/hakurei/dbus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sampleHostPath = "/tmp/bus"
|
||||||
|
sampleHostAddr = "unix:path=" + sampleHostPath
|
||||||
|
sampleBindPath = "/tmp/proxied_bus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var samples = []dbusTestCase{
|
var samples = []dbusTestCase{
|
||||||
@ -19,10 +25,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
"--talk=org.freedesktop.FileManager1",
|
"--talk=org.freedesktop.FileManager1",
|
||||||
@ -48,9 +54,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{"unix:path=/run/dbus/system_bus_socket",
|
[]string{
|
||||||
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
|
sampleHostAddr,
|
||||||
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.bluez",
|
"--talk=org.bluez",
|
||||||
"--talk=org.freedesktop.Avahi",
|
"--talk=org.freedesktop.Avahi",
|
||||||
@ -68,10 +75,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
"--talk=org.kde.StatusNotifierWatcher",
|
"--talk=org.kde.StatusNotifierWatcher",
|
||||||
@ -91,10 +98,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: true,
|
Log: true,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--see=uk.gensokyo.CrashTestDummy1",
|
"--see=uk.gensokyo.CrashTestDummy1",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
@ -114,10 +121,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: true,
|
Log: true,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, true,
|
}, false, true,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--see=uk.gensokyo.CrashTestDummy",
|
"--see=uk.gensokyo.CrashTestDummy",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
|
@ -3,9 +3,7 @@ package dbus_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelperChildStub(t *testing.T) {
|
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
|
||||||
helper.InternalChildStub()
|
|
||||||
}
|
|
||||||
|
82
dist/comp/_hakurei
vendored
Normal file
82
dist/comp/_hakurei
vendored
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#compdef hakurei
|
||||||
|
|
||||||
|
_hakurei_app() {
|
||||||
|
__hakurei_files
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
_hakurei_run() {
|
||||||
|
_arguments \
|
||||||
|
'--id[Reverse-DNS style Application identifier, leave empty to inherit instance identifier]:id' \
|
||||||
|
'-a[Application identity]: :_numbers' \
|
||||||
|
'-g[Groups inherited by all container processes]: :_groups' \
|
||||||
|
'-d[Container home directory]: :_files -/' \
|
||||||
|
'-u[Passwd user name within sandbox]: :_users' \
|
||||||
|
'--wayland[Enable connection to Wayland via security-context-v1]' \
|
||||||
|
'-X[Enable direct connection to X11]' \
|
||||||
|
'--dbus[Enable proxied connection to D-Bus]' \
|
||||||
|
'--pulse[Enable direct connection to PulseAudio]' \
|
||||||
|
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
|
||||||
|
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
|
||||||
|
'--mpris[Allow owning MPRIS D-Bus path]' \
|
||||||
|
'--dbus-log[Force buffered logging in the D-Bus proxy]'
|
||||||
|
}
|
||||||
|
|
||||||
|
_hakurei_ps() {
|
||||||
|
_arguments \
|
||||||
|
'--short[List instances only]'
|
||||||
|
}
|
||||||
|
|
||||||
|
_hakurei_show() {
|
||||||
|
_alternative \
|
||||||
|
'instances:domains:__hakurei_instances' \
|
||||||
|
'files:files:__hakurei_files'
|
||||||
|
}
|
||||||
|
|
||||||
|
__hakurei_files() {
|
||||||
|
_files -g "*.(json|hakurei)"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
__hakurei_instances() {
|
||||||
|
local -a out
|
||||||
|
shift -p
|
||||||
|
out=( ${(f)"$(_call_program commands hakurei ps --short 2>&1)"} )
|
||||||
|
if (( $#out == 0 )); then
|
||||||
|
_message "No active instances"
|
||||||
|
else
|
||||||
|
_describe "active instances" out
|
||||||
|
fi
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
(( $+functions[_hakurei_commands] )) || _hakurei_commands()
|
||||||
|
{
|
||||||
|
local -a _hakurei_cmds
|
||||||
|
_hakurei_cmds=(
|
||||||
|
"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 help message"
|
||||||
|
)
|
||||||
|
if (( CURRENT == 1 )); then
|
||||||
|
_describe -t commands 'action' _hakurei_cmds || compadd "$@"
|
||||||
|
else
|
||||||
|
local curcontext="$curcontext"
|
||||||
|
cmd="${${_hakurei_cmds[(r)$words[1]:*]%%:*}}"
|
||||||
|
if (( $+functions[_hakurei_$cmd] )); then
|
||||||
|
_hakurei_$cmd
|
||||||
|
else
|
||||||
|
_message "no more options"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_arguments -C \
|
||||||
|
'-v[Increase log verbosity]' \
|
||||||
|
'--json[Serialise output in JSON when applicable]' \
|
||||||
|
'*::hakurei command:_hakurei_commands'
|
0
dist/fsurc.default → dist/hsurc.default
vendored
0
dist/fsurc.default → dist/hsurc.default
vendored
12
dist/install.sh
vendored
12
dist/install.sh
vendored
@ -1,12 +1,12 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cd "$(dirname -- "$0")" || exit 1
|
cd "$(dirname -- "$0")" || exit 1
|
||||||
|
|
||||||
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
|
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
|
||||||
install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg"
|
install -vDm0755 "bin/planterette" "${HAKUREI_INSTALL_PREFIX}/usr/bin/planterette"
|
||||||
|
|
||||||
install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
|
install -vDm6511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
|
||||||
if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
|
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
|
||||||
install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"
|
install -vDm0400 "hsurc.default" "${HAKUREI_INSTALL_PREFIX}/etc/hsurc"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
install -vDm0644 "comp/_fortify" "${FORTIFY_INSTALL_PREFIX}/usr/share/zsh/site-functions/_fortify"
|
install -vDm0644 "comp/_hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/share/zsh/site-functions/_hakurei"
|
15
dist/release.sh
vendored
15
dist/release.sh
vendored
@ -1,18 +1,19 @@
|
|||||||
#!/bin/sh -e
|
#!/bin/sh -e
|
||||||
cd "$(dirname -- "$0")/.."
|
cd "$(dirname -- "$0")/.."
|
||||||
VERSION="${FORTIFY_VERSION:-untagged}"
|
VERSION="${HAKUREI_VERSION:-untagged}"
|
||||||
pname="fortify-${VERSION}"
|
pname="hakurei-${VERSION}"
|
||||||
out="dist/${pname}"
|
out="dist/${pname}"
|
||||||
|
|
||||||
mkdir -p "${out}"
|
mkdir -p "${out}"
|
||||||
cp -v "README.md" "dist/fsurc.default" "dist/install.sh" "${out}"
|
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
|
||||||
cp -rv "comp" "${out}"
|
cp -rv "dist/comp" "${out}"
|
||||||
|
|
||||||
go generate ./...
|
go generate ./...
|
||||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
|
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
|
||||||
-X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
|
-X git.gensokyo.uk/security/hakurei/internal.version=${VERSION}
|
||||||
-X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
|
-X git.gensokyo.uk/security/hakurei/internal.hakurei=/usr/bin/hakurei
|
||||||
-X main.Fmain=/usr/bin/fortify" ./...
|
-X git.gensokyo.uk/security/hakurei/internal.hsu=/usr/bin/hsu
|
||||||
|
-X main.hmain=/usr/bin/hakurei" ./...
|
||||||
|
|
||||||
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
|
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
|
||||||
rm -rf "./${out}"
|
rm -rf "./${out}"
|
||||||
|
46
error.go
46
error.go
@ -1,46 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/app"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func logWaitError(err error) {
|
|
||||||
var e *fmsg.BaseError
|
|
||||||
if !fmsg.AsBaseError(err, &e) {
|
|
||||||
log.Println("wait failed:", err)
|
|
||||||
} else {
|
|
||||||
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
|
|
||||||
var se *app.StateStoreError
|
|
||||||
if !errors.As(err, &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 app.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 *fmsg.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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
16
flake.lock
generated
16
flake.lock
generated
@ -7,32 +7,32 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1736373539,
|
"lastModified": 1748665073,
|
||||||
"narHash": "sha256-dinzAqCjenWDxuy+MqUQq0I4zUSfaCvN9rzuCmgMZJY=",
|
"narHash": "sha256-RMhjnPKWtCoIIHiuR9QKD7xfsKb3agxzMfJY8V9MOew=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "bd65bc3cde04c16755955630b344bc9e35272c56",
|
"rev": "282e1e029cb6ab4811114fc85110613d72771dea",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"ref": "release-24.11",
|
"ref": "release-25.05",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1739333913,
|
"lastModified": 1749024892,
|
||||||
"narHash": "sha256-JXt5FtySR+yBm5ny8zG/hX1IybF/7R66jZfXxXSb6wY=",
|
"narHash": "sha256-OGcDEz60TXQC+gVz5sdtgGJdKVYr6rwdzQKuZAJQpCA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7d83f668aee9e41d574c398a9bb569047e8a3f5d",
|
"rev": "8f1b52b04f2cb6e5ead50bd28d76528a2f0380ef",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-24.11-small",
|
"ref": "nixos-25.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
175
flake.nix
175
flake.nix
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
description = "fortify sandbox tool and nixos module";
|
description = "hakurei container tool and nixos module";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
|
|
||||||
home-manager = {
|
home-manager = {
|
||||||
url = "github:nix-community/home-manager/release-24.11";
|
url = "github:nix-community/home-manager/release-25.05";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -27,12 +27,12 @@
|
|||||||
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
|
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
nixosModules.fortify = import ./nixos.nix;
|
nixosModules.hakurei = import ./nixos.nix self.packages;
|
||||||
|
|
||||||
buildPackage = forAllSystems (
|
buildPackage = forAllSystems (
|
||||||
system:
|
system:
|
||||||
nixpkgsFor.${system}.callPackage (
|
nixpkgsFor.${system}.callPackage (
|
||||||
import ./bundle.nix {
|
import ./cmd/planterette/build.nix {
|
||||||
inherit
|
inherit
|
||||||
nixpkgsFor
|
nixpkgsFor
|
||||||
system
|
system
|
||||||
@ -57,18 +57,30 @@
|
|||||||
;
|
;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
check-formatting =
|
hakurei = callPackage ./test { inherit system self; };
|
||||||
runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; }
|
race = callPackage ./test {
|
||||||
''
|
inherit system self;
|
||||||
cd ${./.}
|
withRace = true;
|
||||||
|
};
|
||||||
|
|
||||||
echo "running nixfmt..."
|
sandbox = callPackage ./test/sandbox { inherit self; };
|
||||||
nixfmt --check .
|
sandbox-race = callPackage ./test/sandbox {
|
||||||
|
inherit self;
|
||||||
|
withRace = true;
|
||||||
|
};
|
||||||
|
|
||||||
touch $out
|
planterette = callPackage ./cmd/planterette/test { inherit system self; };
|
||||||
'';
|
|
||||||
|
|
||||||
check-lint =
|
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
||||||
|
cd ${./.}
|
||||||
|
|
||||||
|
echo "running nixfmt..."
|
||||||
|
nixfmt --width=256 --check .
|
||||||
|
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
lint =
|
||||||
runCommandLocal "check-lint"
|
runCommandLocal "check-lint"
|
||||||
{
|
{
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
@ -87,123 +99,68 @@
|
|||||||
|
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
nixos-tests = callPackage ./test.nix {
|
|
||||||
inherit system self home-manager;
|
|
||||||
inherit (self.packages.${system}) fortify;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
packages = forAllSystems (
|
packages = forAllSystems (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
inherit (self.packages.${system}) fortify;
|
inherit (self.packages.${system}) hakurei hsu;
|
||||||
pkgs = nixpkgsFor.${system};
|
pkgs = nixpkgsFor.${system};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = self.packages.${system}.fortify;
|
default = hakurei;
|
||||||
fortify = pkgs.callPackage ./package.nix { };
|
hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
|
||||||
|
inherit (pkgs)
|
||||||
|
# passthru.buildInputs
|
||||||
|
go
|
||||||
|
gcc
|
||||||
|
|
||||||
dist =
|
# nativeBuildInputs
|
||||||
pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; }
|
pkg-config
|
||||||
''
|
wayland-scanner
|
||||||
# go requires XDG_CACHE_HOME for the build cache
|
makeBinaryWrapper
|
||||||
export XDG_CACHE_HOME="$(mktemp -d)"
|
|
||||||
|
|
||||||
# get a different workdir as go does not like /build
|
# appPackages
|
||||||
cd $(mktemp -d) && cp -r ${fortify.src}/. . && chmod -R +w .
|
glibc
|
||||||
|
xdg-dbus-proxy
|
||||||
|
|
||||||
export FORTIFY_VERSION="v${fortify.version}"
|
# planterette
|
||||||
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
|
zstd
|
||||||
'';
|
gnutar
|
||||||
|
coreutils
|
||||||
fhs = pkgs.buildFHSEnv {
|
;
|
||||||
pname = "fortify-fhs";
|
|
||||||
inherit (fortify) version;
|
|
||||||
targetPkgs =
|
|
||||||
pkgs:
|
|
||||||
with pkgs;
|
|
||||||
[
|
|
||||||
go
|
|
||||||
gcc
|
|
||||||
pkg-config
|
|
||||||
wayland-scanner
|
|
||||||
]
|
|
||||||
++ (
|
|
||||||
with pkgs.pkgsStatic;
|
|
||||||
[
|
|
||||||
musl
|
|
||||||
libffi
|
|
||||||
libseccomp
|
|
||||||
acl
|
|
||||||
wayland
|
|
||||||
wayland-protocols
|
|
||||||
]
|
|
||||||
++ (with xorg; [
|
|
||||||
libxcb
|
|
||||||
libXau
|
|
||||||
libXdmcp
|
|
||||||
|
|
||||||
xorgproto
|
|
||||||
])
|
|
||||||
);
|
|
||||||
extraOutputsToInstall = [ "dev" ];
|
|
||||||
profile = ''
|
|
||||||
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
||||||
|
|
||||||
|
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
|
||||||
|
# go requires XDG_CACHE_HOME for the build cache
|
||||||
|
export XDG_CACHE_HOME="$(mktemp -d)"
|
||||||
|
|
||||||
|
# get a different workdir as go does not like /build
|
||||||
|
cd $(mktemp -d) \
|
||||||
|
&& cp -r ${hakurei.src}/. . \
|
||||||
|
&& chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \
|
||||||
|
&& chmod -R +w .
|
||||||
|
|
||||||
|
export HAKUREI_VERSION="v${hakurei.version}"
|
||||||
|
./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
devShells = forAllSystems (
|
devShells = forAllSystems (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
inherit (self.packages.${system}) fortify fhs;
|
inherit (self.packages.${system}) hakurei;
|
||||||
pkgs = nixpkgsFor.${system};
|
pkgs = nixpkgsFor.${system};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
|
||||||
buildInputs =
|
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
|
||||||
with pkgs;
|
|
||||||
[
|
|
||||||
go
|
|
||||||
gcc
|
|
||||||
]
|
|
||||||
# buildInputs
|
|
||||||
++ (
|
|
||||||
with pkgsStatic;
|
|
||||||
[
|
|
||||||
musl
|
|
||||||
libffi
|
|
||||||
libseccomp
|
|
||||||
acl
|
|
||||||
wayland
|
|
||||||
wayland-protocols
|
|
||||||
]
|
|
||||||
++ (with xorg; [
|
|
||||||
libxcb
|
|
||||||
libXau
|
|
||||||
libXdmcp
|
|
||||||
])
|
|
||||||
)
|
|
||||||
# nativeBuildInputs
|
|
||||||
++ [
|
|
||||||
pkg-config
|
|
||||||
wayland-scanner
|
|
||||||
makeBinaryWrapper
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
fhs = fhs.env;
|
|
||||||
|
|
||||||
withPackage = nixpkgsFor.${system}.mkShell {
|
|
||||||
buildInputs = [ self.packages.${system}.fortify ] ++ self.devShells.${system}.default.buildInputs;
|
|
||||||
};
|
|
||||||
|
|
||||||
generateDoc =
|
generateDoc =
|
||||||
let
|
let
|
||||||
pkgs = nixpkgsFor.${system};
|
|
||||||
inherit (pkgs) lib;
|
inherit (pkgs) lib;
|
||||||
|
|
||||||
doc =
|
doc =
|
||||||
@ -212,17 +169,17 @@
|
|||||||
specialArgs = {
|
specialArgs = {
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
};
|
};
|
||||||
modules = [ ./options.nix ];
|
modules = [ (import ./options.nix self.packages) ];
|
||||||
};
|
};
|
||||||
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
|
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
|
||||||
in
|
in
|
||||||
pkgs.nixosOptionsDoc { inherit (cleanEval) options; };
|
pkgs.nixosOptionsDoc { inherit (cleanEval) options; };
|
||||||
docText = pkgs.runCommand "fortify-module-docs.md" { } ''
|
docText = pkgs.runCommand "hakurei-module-docs.md" { } ''
|
||||||
cat ${doc.optionsCommonMark} > $out
|
cat ${doc.optionsCommonMark} > $out
|
||||||
sed -i '/*Declared by:*/,+1 d' $out
|
sed -i '/*Declared by:*/,+1 d' $out
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
nixpkgsFor.${system}.mkShell {
|
pkgs.mkShell {
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
exec cat ${docText} > options.md
|
exec cat ${docText} > options.md
|
||||||
'';
|
'';
|
||||||
|
47
fst/app.go
47
fst/app.go
@ -1,47 +0,0 @@
|
|||||||
package fst
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type App interface {
|
|
||||||
// ID returns a copy of [fst.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 *Config) (SealedApp, error)
|
|
||||||
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SealedApp interface {
|
|
||||||
// Run commits sealed system setup and starts the app process.
|
|
||||||
Run(ctx context.Context, 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
|
|
||||||
// ExitCode is the value returned by shim.
|
|
||||||
ExitCode int
|
|
||||||
// RevertErr is stored by the deferred revert call.
|
|
||||||
RevertErr error
|
|
||||||
// WaitErr is error returned by the underlying wait syscall.
|
|
||||||
WaitErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paths contains environment-dependent paths used by fortify.
|
|
||||||
type Paths struct {
|
|
||||||
// path to shared directory (usually `/tmp/fortify.%d`)
|
|
||||||
SharePath string `json:"share_path"`
|
|
||||||
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
|
|
||||||
RuntimePath string `json:"runtime_path"`
|
|
||||||
// application runtime directory (usually `/run/user/%d/fortify`)
|
|
||||||
RunDirPath string `json:"run_dir_path"`
|
|
||||||
}
|
|
166
fst/config.go
166
fst/config.go
@ -1,166 +0,0 @@
|
|||||||
package fst
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Tmp = "/.fortify"
|
|
||||||
|
|
||||||
// Config is used to seal an app
|
|
||||||
type Config struct {
|
|
||||||
// reverse-DNS style arbitrary identifier string from config;
|
|
||||||
// passed to wayland security-context-v1 as application ID
|
|
||||||
// and used as part of defaults in dbus session proxy
|
|
||||||
ID string `json:"id"`
|
|
||||||
// final argv, passed to init
|
|
||||||
Command []string `json:"command"`
|
|
||||||
|
|
||||||
Confinement ConfinementConfig `json:"confinement"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfinementConfig defines fortified child's confinement
|
|
||||||
type ConfinementConfig struct {
|
|
||||||
// numerical application id, determines uid in the init namespace
|
|
||||||
AppID int `json:"app_id"`
|
|
||||||
// list of supplementary groups to inherit
|
|
||||||
Groups []string `json:"groups"`
|
|
||||||
// passwd username in the sandbox, defaults to passwd name of target uid or chronos
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
// home directory in sandbox, empty for outer
|
|
||||||
Inner string `json:"home_inner"`
|
|
||||||
// home directory in init namespace
|
|
||||||
Outer string `json:"home"`
|
|
||||||
// bwrap sandbox confinement configuration
|
|
||||||
Sandbox *SandboxConfig `json:"sandbox"`
|
|
||||||
// extra acl ops, runs after everything else
|
|
||||||
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
|
|
||||||
|
|
||||||
// reference to a system D-Bus proxy configuration,
|
|
||||||
// nil value disables system bus proxy
|
|
||||||
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
|
||||||
// reference to a session D-Bus proxy configuration,
|
|
||||||
// nil value makes session bus proxy assume built-in defaults
|
|
||||||
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
|
||||||
|
|
||||||
// system resources to expose to the sandbox
|
|
||||||
Enablements system.Enablements `json:"enablements"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExtraPermConfig struct {
|
|
||||||
Ensure bool `json:"ensure,omitempty"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Read bool `json:"r,omitempty"`
|
|
||||||
Write bool `json:"w,omitempty"`
|
|
||||||
Execute bool `json:"x,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ExtraPermConfig) String() string {
|
|
||||||
buf := make([]byte, 0, 5+len(e.Path))
|
|
||||||
buf = append(buf, '-', '-', '-')
|
|
||||||
if e.Ensure {
|
|
||||||
buf = append(buf, '+')
|
|
||||||
}
|
|
||||||
buf = append(buf, ':')
|
|
||||||
buf = append(buf, []byte(e.Path)...)
|
|
||||||
if e.Read {
|
|
||||||
buf[0] = 'r'
|
|
||||||
}
|
|
||||||
if e.Write {
|
|
||||||
buf[1] = 'w'
|
|
||||||
}
|
|
||||||
if e.Execute {
|
|
||||||
buf[2] = 'x'
|
|
||||||
}
|
|
||||||
return string(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilesystemConfig struct {
|
|
||||||
// mount point in sandbox, same as src if empty
|
|
||||||
Dst string `json:"dst,omitempty"`
|
|
||||||
// host filesystem path to make available to sandbox
|
|
||||||
Src string `json:"src"`
|
|
||||||
// write access
|
|
||||||
Write bool `json:"write,omitempty"`
|
|
||||||
// device access
|
|
||||||
Device bool `json:"dev,omitempty"`
|
|
||||||
// fail if mount fails
|
|
||||||
Must bool `json:"require,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template returns a fully populated instance of Config.
|
|
||||||
func Template() *Config {
|
|
||||||
return &Config{
|
|
||||||
ID: "org.chromium.Chromium",
|
|
||||||
Command: []string{
|
|
||||||
"chromium",
|
|
||||||
"--ignore-gpu-blocklist",
|
|
||||||
"--disable-smooth-scrolling",
|
|
||||||
"--enable-features=UseOzonePlatform",
|
|
||||||
"--ozone-platform=wayland",
|
|
||||||
},
|
|
||||||
Confinement: ConfinementConfig{
|
|
||||||
AppID: 9,
|
|
||||||
Groups: []string{"video"},
|
|
||||||
Username: "chronos",
|
|
||||||
Outer: "/var/lib/persist/home/org.chromium.Chromium",
|
|
||||||
Inner: "/var/lib/fortify",
|
|
||||||
Sandbox: &SandboxConfig{
|
|
||||||
Hostname: "localhost",
|
|
||||||
UserNS: true,
|
|
||||||
Net: true,
|
|
||||||
Dev: true,
|
|
||||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
|
||||||
NoNewSession: true,
|
|
||||||
MapRealUID: true,
|
|
||||||
DirectWayland: false,
|
|
||||||
// example API credentials pulled from Google Chrome
|
|
||||||
// DO NOT USE THESE IN A REAL BROWSER
|
|
||||||
Env: map[string]string{
|
|
||||||
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
|
||||||
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
|
||||||
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
|
|
||||||
},
|
|
||||||
Filesystem: []*FilesystemConfig{
|
|
||||||
{Src: "/nix/store"},
|
|
||||||
{Src: "/run/current-system"},
|
|
||||||
{Src: "/run/opengl-driver"},
|
|
||||||
{Src: "/var/db/nix-channels"},
|
|
||||||
{Src: "/var/lib/fortify/u0/org.chromium.Chromium",
|
|
||||||
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
|
|
||||||
{Src: "/dev/dri", Device: true},
|
|
||||||
},
|
|
||||||
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
|
|
||||||
Etc: "/etc",
|
|
||||||
AutoEtc: true,
|
|
||||||
Override: []string{"/var/run/nscd"},
|
|
||||||
},
|
|
||||||
ExtraPerms: []*ExtraPermConfig{
|
|
||||||
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
|
|
||||||
{Path: "/var/lib/fortify/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
|
|
||||||
},
|
|
||||||
SystemBus: &dbus.Config{
|
|
||||||
See: nil,
|
|
||||||
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
|
|
||||||
Own: nil,
|
|
||||||
Call: nil,
|
|
||||||
Broadcast: nil,
|
|
||||||
Log: false,
|
|
||||||
Filter: true,
|
|
||||||
},
|
|
||||||
SessionBus: &dbus.Config{
|
|
||||||
See: nil,
|
|
||||||
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/*"},
|
|
||||||
Log: false,
|
|
||||||
Filter: true,
|
|
||||||
},
|
|
||||||
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
243
fst/sandbox.go
243
fst/sandbox.go
@ -1,243 +0,0 @@
|
|||||||
package fst
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SandboxConfig describes resources made available to the sandbox.
|
|
||||||
type SandboxConfig struct {
|
|
||||||
// unix hostname within sandbox
|
|
||||||
Hostname string `json:"hostname,omitempty"`
|
|
||||||
// allow userns within sandbox
|
|
||||||
UserNS bool `json:"userns,omitempty"`
|
|
||||||
// share net namespace
|
|
||||||
Net bool `json:"net,omitempty"`
|
|
||||||
// share all devices
|
|
||||||
Dev bool `json:"dev,omitempty"`
|
|
||||||
// seccomp syscall filter policy
|
|
||||||
Syscall *bwrap.SyscallPolicy `json:"syscall"`
|
|
||||||
// do not run in new session
|
|
||||||
NoNewSession bool `json:"no_new_session,omitempty"`
|
|
||||||
// map target user uid to privileged user uid in the user namespace
|
|
||||||
MapRealUID bool `json:"map_real_uid"`
|
|
||||||
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
|
|
||||||
// and the bare socket is mounted to the sandbox
|
|
||||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
|
||||||
|
|
||||||
// final environment variables
|
|
||||||
Env map[string]string `json:"env"`
|
|
||||||
// sandbox host filesystem access
|
|
||||||
Filesystem []*FilesystemConfig `json:"filesystem"`
|
|
||||||
// symlinks created inside the sandbox
|
|
||||||
Link [][2]string `json:"symlink"`
|
|
||||||
// read-only /etc directory
|
|
||||||
Etc string `json:"etc,omitempty"`
|
|
||||||
// automatically set up /etc symlinks
|
|
||||||
AutoEtc bool `json:"auto_etc"`
|
|
||||||
// mount tmpfs over these paths,
|
|
||||||
// runs right before [ConfinementConfig.ExtraPerms]
|
|
||||||
Override []string `json:"override"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
|
|
||||||
type SandboxSys interface {
|
|
||||||
Geteuid() int
|
|
||||||
Paths() Paths
|
|
||||||
ReadDir(name string) ([]fs.DirEntry, error)
|
|
||||||
EvalSymlinks(path string) (string, error)
|
|
||||||
|
|
||||||
Println(v ...any)
|
|
||||||
Printf(format string, v ...any)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bwrap returns the address of the corresponding bwrap.Config to s.
|
|
||||||
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
|
|
||||||
func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
|
|
||||||
if s == nil {
|
|
||||||
return nil, errors.New("nil sandbox config")
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Syscall == nil {
|
|
||||||
sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.MapRealUID {
|
|
||||||
// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
|
|
||||||
*uid = 65534
|
|
||||||
} else {
|
|
||||||
// some programs fail to connect to dbus session running as a different uid, so a separate workaround
|
|
||||||
// is introduced to map priv-side caller uid in namespace
|
|
||||||
*uid = sys.Geteuid()
|
|
||||||
}
|
|
||||||
|
|
||||||
conf := (&bwrap.Config{
|
|
||||||
Net: s.Net,
|
|
||||||
UserNS: s.UserNS,
|
|
||||||
UID: uid,
|
|
||||||
GID: uid,
|
|
||||||
Hostname: s.Hostname,
|
|
||||||
Clearenv: true,
|
|
||||||
SetEnv: s.Env,
|
|
||||||
|
|
||||||
/* this is only 4 KiB of memory on a 64-bit system,
|
|
||||||
permissive defaults on NixOS results in around 100 entries
|
|
||||||
so this capacity should eliminate copies for most setups */
|
|
||||||
Filesystem: make([]bwrap.FSBuilder, 0, 256),
|
|
||||||
|
|
||||||
Syscall: s.Syscall,
|
|
||||||
NewSession: !s.NoNewSession,
|
|
||||||
DieWithParent: true,
|
|
||||||
AsInit: true,
|
|
||||||
|
|
||||||
// initialise unconditionally as Once cannot be justified
|
|
||||||
// for saving such a miniscule amount of memory
|
|
||||||
Chmod: make(bwrap.ChmodConfig),
|
|
||||||
}).
|
|
||||||
Procfs("/proc").
|
|
||||||
Tmpfs(Tmp, 4*1024)
|
|
||||||
|
|
||||||
if !s.Dev {
|
|
||||||
conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
|
|
||||||
} else {
|
|
||||||
conf.Bind("/dev", "/dev", false, true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AutoEtc {
|
|
||||||
if s.Etc == "" {
|
|
||||||
conf.Dir("/etc")
|
|
||||||
} else {
|
|
||||||
conf.Bind(s.Etc, "/etc")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieve paths and hide them if they're made available in the sandbox
|
|
||||||
var hidePaths []string
|
|
||||||
sc := sys.Paths()
|
|
||||||
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
|
|
||||||
_, systemBusAddr := dbus.Address()
|
|
||||||
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
|
||||||
return 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 == "/" {
|
|
||||||
sys.Printf("dbus socket %q is in an unusual location", pair[1])
|
|
||||||
}
|
|
||||||
hidePaths = append(hidePaths, dir)
|
|
||||||
} else {
|
|
||||||
sys.Printf("dbus socket %q is not absolute", pair[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hidePathMatch := make([]bool, len(hidePaths))
|
|
||||||
for i := range hidePaths {
|
|
||||||
if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range s.Filesystem {
|
|
||||||
if c == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !path.IsAbs(c.Src) {
|
|
||||||
return 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, fmt.Errorf("dst path %q is not absolute", dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
srcH := c.Src
|
|
||||||
if err := evalSymlinks(sys, &srcH); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range hidePaths {
|
|
||||||
// skip matched entries
|
|
||||||
if hidePathMatch[i] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if ok {
|
|
||||||
hidePathMatch[i] = true
|
|
||||||
sys.Printf("hiding paths from %q", c.Src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide marked paths before setting up shares
|
|
||||||
for i, ok := range hidePathMatch {
|
|
||||||
if ok {
|
|
||||||
conf.Tmpfs(hidePaths[i], 8192)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range s.Link {
|
|
||||||
conf.Symlink(l[0], l[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.AutoEtc {
|
|
||||||
etc := s.Etc
|
|
||||||
if etc == "" {
|
|
||||||
etc = "/etc"
|
|
||||||
}
|
|
||||||
conf.Bind(etc, Tmp+"/etc")
|
|
||||||
|
|
||||||
// link host /etc contents to prevent passwd/group from being overwritten
|
|
||||||
if d, err := sys.ReadDir(etc); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
for _, ent := range d {
|
|
||||||
name := ent.Name()
|
|
||||||
switch name {
|
|
||||||
case "passwd":
|
|
||||||
case "group":
|
|
||||||
|
|
||||||
case "mtab":
|
|
||||||
conf.Symlink("/proc/mounts", "/etc/"+name)
|
|
||||||
default:
|
|
||||||
conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return conf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func evalSymlinks(sys SandboxSys, v *string) error {
|
|
||||||
if p, err := sys.EvalSymlinks(*v); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sys.Printf("path %q does not yet exist", *v)
|
|
||||||
} else {
|
|
||||||
*v = p
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
// Package fst exports shared fortify types.
|
|
||||||
package fst
|
|
4
go.mod
4
go.mod
@ -1,3 +1,3 @@
|
|||||||
module git.gensokyo.uk/security/fortify
|
module git.gensokyo.uk/security/hakurei
|
||||||
|
|
||||||
go 1.22
|
go 1.24
|
||||||
|
@ -1,38 +1,17 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type argsWt [][]byte
|
||||||
ErrContainsNull = errors.New("argument contains null character")
|
|
||||||
)
|
|
||||||
|
|
||||||
type argsWt []string
|
|
||||||
|
|
||||||
// checks whether any element contains the null character
|
|
||||||
// must be called before args use and args must not be modified after call
|
|
||||||
func (a argsWt) check() error {
|
|
||||||
for _, arg := range a {
|
|
||||||
for _, b := range arg {
|
|
||||||
if b == '\x00' {
|
|
||||||
return ErrContainsNull
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a argsWt) WriteTo(w io.Writer) (int64, error) {
|
func (a argsWt) WriteTo(w io.Writer) (int64, error) {
|
||||||
// assuming already checked
|
|
||||||
|
|
||||||
nt := 0
|
nt := 0
|
||||||
// write null terminated arguments
|
|
||||||
for _, arg := range a {
|
for _, arg := range a {
|
||||||
n, err := w.Write([]byte(arg + "\x00"))
|
n, err := w.Write(arg)
|
||||||
nt += n
|
nt += n
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -44,18 +23,32 @@ func (a argsWt) WriteTo(w io.Writer) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a argsWt) String() string {
|
func (a argsWt) String() string {
|
||||||
return strings.Join(a, " ")
|
return string(
|
||||||
|
bytes.TrimSuffix(
|
||||||
|
bytes.ReplaceAll(
|
||||||
|
bytes.Join(a, nil),
|
||||||
|
[]byte{0}, []byte{' '},
|
||||||
|
),
|
||||||
|
[]byte{' '},
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCheckedArgs returns a checked argument writer for args.
|
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||||
// Callers must not retain any references to args.
|
func NewCheckedArgs(args []string) (wt io.WriterTo, err error) {
|
||||||
func NewCheckedArgs(args []string) (io.WriterTo, error) {
|
a := make(argsWt, len(args))
|
||||||
a := argsWt(args)
|
for i, arg := range args {
|
||||||
return a, a.check()
|
a[i], err = syscall.ByteSliceFromString(arg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wt = a
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustNewCheckedArgs returns a checked argument writer for args and panics if check fails.
|
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||||
// Callers must not retain any references to args.
|
// If s contains a NUL byte this function panics instead of returning an error.
|
||||||
func MustNewCheckedArgs(args []string) io.WriterTo {
|
func MustNewCheckedArgs(args []string) io.WriterTo {
|
||||||
a, err := NewCheckedArgs(args)
|
a, err := NewCheckedArgs(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4,34 +4,33 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_argsFD_String(t *testing.T) {
|
func TestArgsString(t *testing.T) {
|
||||||
wantString := strings.Join(wantArgs, " ")
|
wantString := strings.Join(wantArgs, " ")
|
||||||
if got := argsWt.(fmt.Stringer).String(); got != wantString {
|
if got := argsWt.(fmt.Stringer).String(); got != wantString {
|
||||||
t.Errorf("String(): got %v; want %v",
|
t.Errorf("String: %q, want %q",
|
||||||
got, wantString)
|
got, wantString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewCheckedArgs(t *testing.T) {
|
func TestNewCheckedArgs(t *testing.T) {
|
||||||
args := []string{"\x00"}
|
args := []string{"\x00"}
|
||||||
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, helper.ErrContainsNull) {
|
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, syscall.EINVAL) {
|
||||||
t.Errorf("NewCheckedArgs(%q) error = %v, wantErr %v",
|
t.Errorf("NewCheckedArgs: error = %v, wantErr %v",
|
||||||
args,
|
err, syscall.EINVAL)
|
||||||
err, helper.ErrContainsNull)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("must panic", func(t *testing.T) {
|
t.Run("must panic", func(t *testing.T) {
|
||||||
badPayload := []string{"\x00"}
|
badPayload := []string{"\x00"}
|
||||||
defer func() {
|
defer func() {
|
||||||
wantPanic := "argument contains null character"
|
wantPanic := "invalid argument"
|
||||||
if r := recover(); r != wantPanic {
|
if r := recover(); r != wantPanic {
|
||||||
t.Errorf("MustNewCheckedArgs(%q) panic = %v, wantPanic %v",
|
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v",
|
||||||
badPayload,
|
|
||||||
r, wantPanic)
|
r, wantPanic)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BubblewrapName is the file name or path to bubblewrap.
|
|
||||||
var BubblewrapName = "bwrap"
|
|
||||||
|
|
||||||
type bubblewrap struct {
|
|
||||||
// final args fd of bwrap process
|
|
||||||
argsFd uintptr
|
|
||||||
|
|
||||||
// name of the command to run in bwrap
|
|
||||||
name string
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
*helperCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
|
|
||||||
b.lock.Lock()
|
|
||||||
defer b.lock.Unlock()
|
|
||||||
|
|
||||||
// Check for doubled Start calls before we defer failure cleanup. If the prior
|
|
||||||
// call to Start succeeded, we don't want to spuriously close its pipes.
|
|
||||||
if b.Cmd != nil && b.Cmd.Process != nil {
|
|
||||||
return errors.New("exec: already started")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := b.finalise(ctx, stat)
|
|
||||||
b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
|
|
||||||
b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
|
|
||||||
b.Cmd.Args = append(b.Cmd.Args, args...)
|
|
||||||
return proc.Fulfill(ctx, b.Cmd, b.files, b.extraFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
|
|
||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
|
||||||
// Function argF returns an array of arguments passed directly to the child process.
|
|
||||||
func MustNewBwrap(
|
|
||||||
conf *bwrap.Config, name string,
|
|
||||||
wt io.WriterTo, argF func(argsFD, statFD int) []string,
|
|
||||||
extraFiles []*os.File,
|
|
||||||
syncFd *os.File,
|
|
||||||
) Helper {
|
|
||||||
b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
} else {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
|
|
||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
|
||||||
// Function argF returns an array of arguments passed directly to the child process.
|
|
||||||
func NewBwrap(
|
|
||||||
conf *bwrap.Config, name string,
|
|
||||||
wt io.WriterTo, argF func(argsFd, statFd int) []string,
|
|
||||||
extraFiles []*os.File,
|
|
||||||
syncFd *os.File,
|
|
||||||
) (Helper, error) {
|
|
||||||
b := new(bubblewrap)
|
|
||||||
|
|
||||||
b.name = name
|
|
||||||
b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
|
|
||||||
|
|
||||||
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
f := proc.NewWriterTo(v)
|
|
||||||
b.argsFd = proc.InitFile(f, b.extraFiles)
|
|
||||||
b.files = append(b.files, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
package bwrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Builder interface {
|
|
||||||
Len() int
|
|
||||||
Append(args *[]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FSBuilder interface {
|
|
||||||
Path() string
|
|
||||||
Builder
|
|
||||||
}
|
|
||||||
|
|
||||||
type FDBuilder interface {
|
|
||||||
proc.File
|
|
||||||
Builder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Args returns a slice of bwrap args corresponding to c.
|
|
||||||
func (c *Config) Args(syncFd *os.File, extraFiles *proc.ExtraFilesPre, files *[]proc.File) (args []string) {
|
|
||||||
builders := []Builder{
|
|
||||||
c.boolArgs(),
|
|
||||||
c.intArgs(),
|
|
||||||
c.stringArgs(),
|
|
||||||
c.pairArgs(),
|
|
||||||
c.seccompArgs(),
|
|
||||||
newFile(SyncFd.String(), syncFd),
|
|
||||||
}
|
|
||||||
|
|
||||||
builders = slices.Grow(builders, len(c.Filesystem)+1)
|
|
||||||
for _, f := range c.Filesystem {
|
|
||||||
builders = append(builders, f)
|
|
||||||
}
|
|
||||||
builders = append(builders, c.Chmod)
|
|
||||||
|
|
||||||
argc := 0
|
|
||||||
fc := 0
|
|
||||||
for _, b := range builders {
|
|
||||||
l := b.Len()
|
|
||||||
if l < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
argc += l
|
|
||||||
|
|
||||||
if f, ok := b.(FDBuilder); ok {
|
|
||||||
fc++
|
|
||||||
proc.InitFile(f, extraFiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fc++ // allocate extra slot for stat fd
|
|
||||||
|
|
||||||
args = make([]string, 0, argc)
|
|
||||||
*files = slices.Grow(*files, fc)
|
|
||||||
for _, b := range builders {
|
|
||||||
if b.Len() < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b.Append(&args)
|
|
||||||
|
|
||||||
if f, ok := b.(FDBuilder); ok {
|
|
||||||
*files = append(*files, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,199 +0,0 @@
|
|||||||
package bwrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
Bind binds mount src on host to dest in sandbox.
|
|
||||||
|
|
||||||
Bind(src, dest) bind mount host path readonly on sandbox
|
|
||||||
(--ro-bind SRC DEST).
|
|
||||||
Bind(src, dest, true) equal to ROBind but ignores non-existent host path
|
|
||||||
(--ro-bind-try SRC DEST).
|
|
||||||
|
|
||||||
Bind(src, dest, false, true) bind mount host path on sandbox.
|
|
||||||
(--bind SRC DEST).
|
|
||||||
Bind(src, dest, true, true) equal to Bind but ignores non-existent host path
|
|
||||||
(--bind-try SRC DEST).
|
|
||||||
|
|
||||||
Bind(src, dest, false, true, true) bind mount host path on sandbox, allowing device access
|
|
||||||
(--dev-bind SRC DEST).
|
|
||||||
Bind(src, dest, true, true, true) equal to DevBind but ignores non-existent host path
|
|
||||||
(--dev-bind-try SRC DEST).
|
|
||||||
*/
|
|
||||||
func (c *Config) Bind(src, dest string, opts ...bool) *Config {
|
|
||||||
var (
|
|
||||||
try bool
|
|
||||||
write bool
|
|
||||||
dev bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(opts) > 0 {
|
|
||||||
try = opts[0]
|
|
||||||
}
|
|
||||||
if len(opts) > 1 {
|
|
||||||
write = opts[1]
|
|
||||||
}
|
|
||||||
if len(opts) > 2 {
|
|
||||||
dev = opts[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
if dev {
|
|
||||||
if try {
|
|
||||||
c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.String(), src, dest})
|
|
||||||
} else {
|
|
||||||
c.Filesystem = append(c.Filesystem, &pairF{DevBind.String(), src, dest})
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
} else if write {
|
|
||||||
if try {
|
|
||||||
c.Filesystem = append(c.Filesystem, &pairF{BindTry.String(), src, dest})
|
|
||||||
} else {
|
|
||||||
c.Filesystem = append(c.Filesystem, &pairF{Bind.String(), src, dest})
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
} else {
|
|
||||||
if try {
|
|
||||||
c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.String(), src, dest})
|
|
||||||
} else {
|
|
||||||
c.Filesystem = append(c.Filesystem, &pairF{ROBind.String(), src, dest})
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFile copy from FD to destination DEST
|
|
||||||
// (--file FD DEST)
|
|
||||||
func (c *Config) WriteFile(name string, data []byte) *Config {
|
|
||||||
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: name, Data: data, Type: DataWrite})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
CopyBind copy from FD to file which is readonly bind-mounted on DEST
|
|
||||||
(--ro-bind-data FD DEST)
|
|
||||||
|
|
||||||
CopyBind(dest, payload, true) copy from FD to file which is bind-mounted on DEST
|
|
||||||
(--bind-data FD DEST)
|
|
||||||
*/
|
|
||||||
func (c *Config) CopyBind(dest string, payload []byte, opts ...bool) *Config {
|
|
||||||
var p *[]byte
|
|
||||||
c.CopyBindRef(dest, &p, opts...)
|
|
||||||
*p = payload
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyBindRef is the same as CopyBind but writes the address of DataConfig.Data.
|
|
||||||
func (c *Config) CopyBindRef(dest string, payloadRef **[]byte, opts ...bool) *Config {
|
|
||||||
t := DataROBind
|
|
||||||
if len(opts) > 0 && opts[0] {
|
|
||||||
t = DataBind
|
|
||||||
}
|
|
||||||
d := &DataConfig{Dest: dest, Type: t}
|
|
||||||
*payloadRef = &d.Data
|
|
||||||
|
|
||||||
c.Filesystem = append(c.Filesystem, d)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dir create dir in sandbox
|
|
||||||
// (--dir DEST)
|
|
||||||
func (c *Config) Dir(dest string) *Config {
|
|
||||||
c.Filesystem = append(c.Filesystem, &stringF{Dir.String(), dest})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemountRO remount path as readonly; does not recursively remount
|
|
||||||
// (--remount-ro DEST)
|
|
||||||
func (c *Config) RemountRO(dest string) *Config {
|
|
||||||
c.Filesystem = append(c.Filesystem, &stringF{RemountRO.String(), dest})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Procfs mount new procfs in sandbox
|
|
||||||
// (--proc DEST)
|
|
||||||
func (c *Config) Procfs(dest string) *Config {
|
|
||||||
c.Filesystem = append(c.Filesystem, &stringF{Procfs.String(), dest})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// DevTmpfs mount new dev in sandbox
|
|
||||||
// (--dev DEST)
|
|
||||||
func (c *Config) DevTmpfs(dest string) *Config {
|
|
||||||
c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.String(), dest})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mqueue mount new mqueue in sandbox
|
|
||||||
// (--mqueue DEST)
|
|
||||||
func (c *Config) Mqueue(dest string) *Config {
|
|
||||||
c.Filesystem = append(c.Filesystem, &stringF{Mqueue.String(), dest})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tmpfs mount new tmpfs in sandbox
|
|
||||||
// (--tmpfs DEST)
|
|
||||||
func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config {
|
|
||||||
tmpfs := &PermConfig[*TmpfsConfig]{Inner: &TmpfsConfig{Dir: dest}}
|
|
||||||
if size >= 0 {
|
|
||||||
tmpfs.Inner.Size = size
|
|
||||||
}
|
|
||||||
if len(perm) == 1 {
|
|
||||||
tmpfs.Mode = &perm[0]
|
|
||||||
}
|
|
||||||
c.Filesystem = append(c.Filesystem, tmpfs)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overlay mount overlayfs on DEST, with writes going to an invisible tmpfs
|
|
||||||
// (--tmp-overlay DEST)
|
|
||||||
func (c *Config) Overlay(dest string, src ...string) *Config {
|
|
||||||
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join mount overlayfs read-only on DEST
|
|
||||||
// (--ro-overlay DEST)
|
|
||||||
func (c *Config) Join(dest string, src ...string) *Config {
|
|
||||||
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: new([2]string)})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist mount overlayfs on DEST, with RWSRC as the host path for writes and
|
|
||||||
// WORKDIR an empty directory on the same filesystem as RWSRC
|
|
||||||
// (--overlay RWSRC WORKDIR DEST)
|
|
||||||
func (c *Config) Persist(dest, rwsrc, workdir string, src ...string) *Config {
|
|
||||||
if rwsrc == "" || workdir == "" {
|
|
||||||
panic("persist called without required paths")
|
|
||||||
}
|
|
||||||
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: &[2]string{rwsrc, workdir}})
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Symlink create symlink within sandbox
|
|
||||||
// (--symlink SRC DEST)
|
|
||||||
func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config {
|
|
||||||
symlink := &PermConfig[SymlinkConfig]{Inner: SymlinkConfig{src, dest}}
|
|
||||||
if len(perm) == 1 {
|
|
||||||
symlink.Mode = &perm[0]
|
|
||||||
}
|
|
||||||
c.Filesystem = append(c.Filesystem, symlink)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUID sets custom uid in the sandbox, requires new user namespace (--uid UID).
|
|
||||||
func (c *Config) SetUID(uid int) *Config {
|
|
||||||
if uid >= 0 {
|
|
||||||
c.UID = &uid
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGID sets custom gid in the sandbox, requires new user namespace (--gid GID).
|
|
||||||
func (c *Config) SetGID(gid int) *Config {
|
|
||||||
if gid >= 0 {
|
|
||||||
c.GID = &gid
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
package bwrap
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
// unshare every namespace we support by default if nil
|
|
||||||
// (--unshare-all)
|
|
||||||
Unshare *UnshareConfig `json:"unshare,omitempty"`
|
|
||||||
// retain the network namespace (can only combine with nil Unshare)
|
|
||||||
// (--share-net)
|
|
||||||
Net bool `json:"net"`
|
|
||||||
|
|
||||||
// disable further use of user namespaces inside sandbox and fail unless
|
|
||||||
// further use of user namespace inside sandbox is disabled if false
|
|
||||||
// (--disable-userns) (--assert-userns-disabled)
|
|
||||||
UserNS bool `json:"userns"`
|
|
||||||
|
|
||||||
// custom uid in the sandbox, requires new user namespace
|
|
||||||
// (--uid UID)
|
|
||||||
UID *int `json:"uid,omitempty"`
|
|
||||||
// custom gid in the sandbox, requires new user namespace
|
|
||||||
// (--gid GID)
|
|
||||||
GID *int `json:"gid,omitempty"`
|
|
||||||
// custom hostname in the sandbox, requires new uts namespace
|
|
||||||
// (--hostname NAME)
|
|
||||||
Hostname string `json:"hostname,omitempty"`
|
|
||||||
|
|
||||||
// change directory
|
|
||||||
// (--chdir DIR)
|
|
||||||
Chdir string `json:"chdir,omitempty"`
|
|
||||||
// unset all environment variables
|
|
||||||
// (--clearenv)
|
|
||||||
Clearenv bool `json:"clearenv"`
|
|
||||||
// set environment variable
|
|
||||||
// (--setenv VAR VALUE)
|
|
||||||
SetEnv map[string]string `json:"setenv,omitempty"`
|
|
||||||
// unset environment variables
|
|
||||||
// (--unsetenv VAR)
|
|
||||||
UnsetEnv []string `json:"unsetenv,omitempty"`
|
|
||||||
|
|
||||||
// take a lock on file while sandbox is running
|
|
||||||
// (--lock-file DEST)
|
|
||||||
LockFile []string `json:"lock_file,omitempty"`
|
|
||||||
|
|
||||||
// ordered filesystem args
|
|
||||||
Filesystem []FSBuilder `json:"filesystem,omitempty"`
|
|
||||||
|
|
||||||
// change permissions (must already exist)
|
|
||||||
// (--chmod OCTAL PATH)
|
|
||||||
Chmod ChmodConfig `json:"chmod,omitempty"`
|
|
||||||
|
|
||||||
// load and use seccomp rules from FD (not repeatable)
|
|
||||||
// (--seccomp FD)
|
|
||||||
Syscall *SyscallPolicy
|
|
||||||
|
|
||||||
// create a new terminal session
|
|
||||||
// (--new-session)
|
|
||||||
NewSession bool `json:"new_session"`
|
|
||||||
// kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.
|
|
||||||
// (--die-with-parent)
|
|
||||||
DieWithParent bool `json:"die_with_parent"`
|
|
||||||
// do not install a reaper process with PID=1
|
|
||||||
// (--as-pid-1)
|
|
||||||
AsInit bool `json:"as_init"`
|
|
||||||
|
|
||||||
/* unmapped options include:
|
|
||||||
--unshare-user-try Create new user namespace if possible else continue by skipping it
|
|
||||||
--unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it
|
|
||||||
--userns FD Use this user namespace (cannot combine with --unshare-user)
|
|
||||||
--userns2 FD After setup switch to this user namespace, only useful with --userns
|
|
||||||
--pidns FD Use this pid namespace (as parent namespace if using --unshare-pid)
|
|
||||||
--bind-fd FD DEST Bind open directory or path fd on DEST
|
|
||||||
--ro-bind-fd FD DEST Bind open directory or path fd read-only on DEST
|
|
||||||
--exec-label LABEL Exec label for the sandbox
|
|
||||||
--file-label LABEL File label for temporary sandbox content
|
|
||||||
--add-seccomp-fd FD Load and use seccomp rules from FD (repeatable)
|
|
||||||
--block-fd FD Block on FD until some data to read is available
|
|
||||||
--userns-block-fd FD Block on FD until the user namespace is ready
|
|
||||||
--info-fd FD Write information about the running container to FD
|
|
||||||
--json-status-fd FD Write container status to FD as multiple JSON documents
|
|
||||||
--cap-add CAP Add cap CAP when running as privileged user
|
|
||||||
--cap-drop CAP Drop cap CAP when running as privileged user
|
|
||||||
|
|
||||||
among which --args is used internally for passing arguments */
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnshareConfig struct {
|
|
||||||
// (--unshare-user)
|
|
||||||
// create new user namespace
|
|
||||||
User bool `json:"user"`
|
|
||||||
// (--unshare-ipc)
|
|
||||||
// create new ipc namespace
|
|
||||||
IPC bool `json:"ipc"`
|
|
||||||
// (--unshare-pid)
|
|
||||||
// create new pid namespace
|
|
||||||
PID bool `json:"pid"`
|
|
||||||
// (--unshare-net)
|
|
||||||
// create new network namespace
|
|
||||||
Net bool `json:"net"`
|
|
||||||
// (--unshare-uts)
|
|
||||||
// create new uts namespace
|
|
||||||
UTS bool `json:"uts"`
|
|
||||||
// (--unshare-cgroup)
|
|
||||||
// create new cgroup namespace
|
|
||||||
CGroup bool `json:"cgroup"`
|
|
||||||
}
|
|
@ -1,257 +0,0 @@
|
|||||||
package bwrap_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConfig_Args(t *testing.T) {
|
|
||||||
seccomp.CPrintln = log.Println
|
|
||||||
t.Cleanup(func() { seccomp.CPrintln = nil })
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
conf *bwrap.Config
|
|
||||||
want []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"bind", (new(bwrap.Config)).
|
|
||||||
Bind("/etc", "/.fortify/etc").
|
|
||||||
Bind("/etc", "/.fortify/etc", true).
|
|
||||||
Bind("/run", "/.fortify/run", false, true).
|
|
||||||
Bind("/sys/devices", "/.fortify/sys/devices", true, true).
|
|
||||||
Bind("/dev/dri", "/.fortify/dev/dri", false, true, true).
|
|
||||||
Bind("/dev/dri", "/.fortify/dev/dri", true, true, true),
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
// Bind("/etc", "/.fortify/etc")
|
|
||||||
"--ro-bind", "/etc", "/.fortify/etc",
|
|
||||||
// Bind("/etc", "/.fortify/etc", true)
|
|
||||||
"--ro-bind-try", "/etc", "/.fortify/etc",
|
|
||||||
// Bind("/run", "/.fortify/run", false, true)
|
|
||||||
"--bind", "/run", "/.fortify/run",
|
|
||||||
// Bind("/sys/devices", "/.fortify/sys/devices", true, true)
|
|
||||||
"--bind-try", "/sys/devices", "/.fortify/sys/devices",
|
|
||||||
// Bind("/dev/dri", "/.fortify/dev/dri", false, true, true)
|
|
||||||
"--dev-bind", "/dev/dri", "/.fortify/dev/dri",
|
|
||||||
// Bind("/dev/dri", "/.fortify/dev/dri", true, true, true)
|
|
||||||
"--dev-bind-try", "/dev/dri", "/.fortify/dev/dri",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dir remount-ro proc dev mqueue", (new(bwrap.Config)).
|
|
||||||
Dir("/.fortify").
|
|
||||||
RemountRO("/home").
|
|
||||||
Procfs("/proc").
|
|
||||||
DevTmpfs("/dev").
|
|
||||||
Mqueue("/dev/mqueue"),
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
// Dir("/.fortify")
|
|
||||||
"--dir", "/.fortify",
|
|
||||||
// RemountRO("/home")
|
|
||||||
"--remount-ro", "/home",
|
|
||||||
// Procfs("/proc")
|
|
||||||
"--proc", "/proc",
|
|
||||||
// DevTmpfs("/dev")
|
|
||||||
"--dev", "/dev",
|
|
||||||
// Mqueue("/dev/mqueue")
|
|
||||||
"--mqueue", "/dev/mqueue",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tmpfs", (new(bwrap.Config)).
|
|
||||||
Tmpfs("/run/user", 8192).
|
|
||||||
Tmpfs("/run/dbus", 8192, 0755),
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
// Tmpfs("/run/user", 8192)
|
|
||||||
"--size", "8192", "--tmpfs", "/run/user",
|
|
||||||
// Tmpfs("/run/dbus", 8192, 0755)
|
|
||||||
"--perms", "755", "--size", "8192", "--tmpfs", "/run/dbus",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"symlink", (new(bwrap.Config)).
|
|
||||||
Symlink("/.fortify/sbin/init", "/sbin/init").
|
|
||||||
Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
// Symlink("/.fortify/sbin/init", "/sbin/init")
|
|
||||||
"--symlink", "/.fortify/sbin/init", "/sbin/init",
|
|
||||||
// Symlink("/.fortify/sbin/init", "/sbin/init", 0755)
|
|
||||||
"--perms", "755", "--symlink", "/.fortify/sbin/init", "/sbin/init",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"overlayfs", (new(bwrap.Config)).
|
|
||||||
Overlay("/etc", "/etc").
|
|
||||||
Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin").
|
|
||||||
Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"),
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
// Overlay("/etc", "/etc")
|
|
||||||
"--overlay-src", "/etc", "--tmp-overlay", "/etc",
|
|
||||||
// Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin")
|
|
||||||
"--overlay-src", "/bin", "--overlay-src", "/usr/bin",
|
|
||||||
"--overlay-src", "/usr/local/bin", "--ro-overlay", "/.fortify/bin",
|
|
||||||
// Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix")
|
|
||||||
"--overlay-src", "/data/app/org.chromium.Chromium/nix",
|
|
||||||
"--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"copy", (new(bwrap.Config)).
|
|
||||||
WriteFile("/.fortify/version", make([]byte, 8)).
|
|
||||||
CopyBind("/etc/group", make([]byte, 8)).
|
|
||||||
CopyBind("/etc/passwd", make([]byte, 8), true),
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
// Write("/.fortify/version", make([]byte, 8))
|
|
||||||
"--file", "3", "/.fortify/version",
|
|
||||||
// CopyBind("/etc/group", make([]byte, 8))
|
|
||||||
"--ro-bind-data", "4", "/etc/group",
|
|
||||||
// CopyBind("/etc/passwd", make([]byte, 8), true)
|
|
||||||
"--bind-data", "5", "/etc/passwd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"unshare", &bwrap.Config{Unshare: &bwrap.UnshareConfig{
|
|
||||||
User: false,
|
|
||||||
IPC: false,
|
|
||||||
PID: false,
|
|
||||||
Net: false,
|
|
||||||
UTS: false,
|
|
||||||
CGroup: false,
|
|
||||||
}},
|
|
||||||
[]string{"--disable-userns", "--assert-userns-disabled"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uid gid sync", (new(bwrap.Config)).
|
|
||||||
SetUID(1971).
|
|
||||||
SetGID(100),
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
// SetUID(1971)
|
|
||||||
"--uid", "1971",
|
|
||||||
// SetGID(100)
|
|
||||||
"--gid", "100",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hostname chdir setenv unsetenv lockfile chmod syscall", &bwrap.Config{
|
|
||||||
Hostname: "fortify",
|
|
||||||
Chdir: "/.fortify",
|
|
||||||
SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
|
|
||||||
UnsetEnv: []string{"HOME", "HOST"},
|
|
||||||
LockFile: []string{"/.fortify/lock"},
|
|
||||||
Syscall: new(bwrap.SyscallPolicy),
|
|
||||||
Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755},
|
|
||||||
},
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
// Hostname: "fortify"
|
|
||||||
"--hostname", "fortify",
|
|
||||||
// Chdir: "/.fortify"
|
|
||||||
"--chdir", "/.fortify",
|
|
||||||
// UnsetEnv: []string{"HOME", "HOST"}
|
|
||||||
"--unsetenv", "HOME",
|
|
||||||
"--unsetenv", "HOST",
|
|
||||||
// LockFile: []string{"/.fortify/lock"},
|
|
||||||
"--lock-file", "/.fortify/lock",
|
|
||||||
// SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}
|
|
||||||
"--setenv", "FORTIFY_INIT", "/.fortify/sbin/init",
|
|
||||||
// Syscall: new(bwrap.SyscallPolicy),
|
|
||||||
"--seccomp", "3",
|
|
||||||
// Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}
|
|
||||||
"--chmod", "755", "/.fortify/sbin/init",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"xdg-dbus-proxy constraint sample", (&bwrap.Config{Clearenv: true, DieWithParent: true}).
|
|
||||||
Symlink("usr/bin", "/bin").
|
|
||||||
Symlink("var/home", "/home").
|
|
||||||
Symlink("usr/lib", "/lib").
|
|
||||||
Symlink("usr/lib64", "/lib64").
|
|
||||||
Symlink("run/media", "/media").
|
|
||||||
Symlink("var/mnt", "/mnt").
|
|
||||||
Symlink("var/opt", "/opt").
|
|
||||||
Symlink("sysroot/ostree", "/ostree").
|
|
||||||
Symlink("var/roothome", "/root").
|
|
||||||
Symlink("usr/sbin", "/sbin").
|
|
||||||
Symlink("var/srv", "/srv").
|
|
||||||
Bind("/run", "/run", false, true).
|
|
||||||
Bind("/tmp", "/tmp", false, true).
|
|
||||||
Bind("/var", "/var", false, true).
|
|
||||||
Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true).
|
|
||||||
Bind("/boot", "/boot").
|
|
||||||
Bind("/dev", "/dev").
|
|
||||||
Bind("/proc", "/proc").
|
|
||||||
Bind("/sys", "/sys").
|
|
||||||
Bind("/sysroot", "/sysroot").
|
|
||||||
Bind("/usr", "/usr").
|
|
||||||
Bind("/etc", "/etc"),
|
|
||||||
[]string{
|
|
||||||
"--unshare-all", "--unshare-user",
|
|
||||||
"--disable-userns", "--assert-userns-disabled",
|
|
||||||
"--clearenv", "--die-with-parent",
|
|
||||||
"--symlink", "usr/bin", "/bin",
|
|
||||||
"--symlink", "var/home", "/home",
|
|
||||||
"--symlink", "usr/lib", "/lib",
|
|
||||||
"--symlink", "usr/lib64", "/lib64",
|
|
||||||
"--symlink", "run/media", "/media",
|
|
||||||
"--symlink", "var/mnt", "/mnt",
|
|
||||||
"--symlink", "var/opt", "/opt",
|
|
||||||
"--symlink", "sysroot/ostree", "/ostree",
|
|
||||||
"--symlink", "var/roothome", "/root",
|
|
||||||
"--symlink", "usr/sbin", "/sbin",
|
|
||||||
"--symlink", "var/srv", "/srv",
|
|
||||||
"--bind", "/run", "/run",
|
|
||||||
"--bind", "/tmp", "/tmp",
|
|
||||||
"--bind", "/var", "/var",
|
|
||||||
"--bind", "/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/",
|
|
||||||
"--ro-bind", "/boot", "/boot",
|
|
||||||
"--ro-bind", "/dev", "/dev",
|
|
||||||
"--ro-bind", "/proc", "/proc",
|
|
||||||
"--ro-bind", "/sys", "/sys",
|
|
||||||
"--ro-bind", "/sysroot", "/sysroot",
|
|
||||||
"--ro-bind", "/usr", "/usr",
|
|
||||||
"--ro-bind", "/etc", "/etc",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
if got := tc.conf.Args(nil, new(proc.ExtraFilesPre), new([]proc.File)); !slices.Equal(got, tc.want) {
|
|
||||||
t.Errorf("Args() = %#v, want %#v", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// test persist validation
|
|
||||||
t.Run("invalid persist", func(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
wantPanic := "persist called without required paths"
|
|
||||||
if r := recover(); r != wantPanic {
|
|
||||||
t.Errorf("Persist() panic = %v; wantPanic %v", r, wantPanic)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
(new(bwrap.Config)).Persist("/run", "", "")
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
package bwrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SyscallPolicy struct {
|
|
||||||
// disable fortify extensions
|
|
||||||
Compat bool `json:"compat"`
|
|
||||||
// deny development syscalls
|
|
||||||
DenyDevel bool `json:"deny_devel"`
|
|
||||||
// deny multiarch/emulation syscalls
|
|
||||||
Multiarch bool `json:"multiarch"`
|
|
||||||
// allow PER_LINUX32
|
|
||||||
Linux32 bool `json:"linux32"`
|
|
||||||
// allow AF_CAN
|
|
||||||
Can bool `json:"can"`
|
|
||||||
// allow AF_BLUETOOTH
|
|
||||||
Bluetooth bool `json:"bluetooth"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) seccompArgs() FDBuilder {
|
|
||||||
// explicitly disable syscall filter
|
|
||||||
if c.Syscall == nil {
|
|
||||||
// nil File skips builder
|
|
||||||
return new(seccompBuilder)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
opts seccomp.SyscallOpts
|
|
||||||
optd []string
|
|
||||||
optCond = [...]struct {
|
|
||||||
v bool
|
|
||||||
o seccomp.SyscallOpts
|
|
||||||
d string
|
|
||||||
}{
|
|
||||||
{!c.Syscall.Compat, seccomp.FlagExt, "fortify"},
|
|
||||||
{!c.UserNS, seccomp.FlagDenyNS, "denyns"},
|
|
||||||
{c.NewSession, seccomp.FlagDenyTTY, "denytty"},
|
|
||||||
{c.Syscall.DenyDevel, seccomp.FlagDenyDevel, "denydevel"},
|
|
||||||
{c.Syscall.Multiarch, seccomp.FlagMultiarch, "multiarch"},
|
|
||||||
{c.Syscall.Linux32, seccomp.FlagLinux32, "linux32"},
|
|
||||||
{c.Syscall.Can, seccomp.FlagCan, "can"},
|
|
||||||
{c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if seccomp.CPrintln != nil {
|
|
||||||
optd = make([]string, 1, len(optCond)+1)
|
|
||||||
optd[0] = "common"
|
|
||||||
}
|
|
||||||
for _, opt := range optCond {
|
|
||||||
if opt.v {
|
|
||||||
opts |= opt.o
|
|
||||||
if seccomp.CPrintln != nil {
|
|
||||||
optd = append(optd, opt.d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if seccomp.CPrintln != nil {
|
|
||||||
seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &seccompBuilder{seccomp.NewFile(opts)}
|
|
||||||
}
|
|
||||||
|
|
||||||
type seccompBuilder struct{ proc.File }
|
|
||||||
|
|
||||||
func (s *seccompBuilder) Len() int {
|
|
||||||
if s == nil || s.File == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *seccompBuilder) Append(args *[]string) {
|
|
||||||
if s == nil || s.File == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*args = append(*args, Seccomp.String(), strconv.Itoa(int(s.Fd())))
|
|
||||||
}
|
|
@ -1,273 +0,0 @@
|
|||||||
package bwrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/gob"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
gob.Register(new(PermConfig[SymlinkConfig]))
|
|
||||||
gob.Register(new(PermConfig[*TmpfsConfig]))
|
|
||||||
gob.Register(new(OverlayConfig))
|
|
||||||
gob.Register(new(DataConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
type PositionalArg int
|
|
||||||
|
|
||||||
func (p PositionalArg) String() string { return positionalArgs[p] }
|
|
||||||
|
|
||||||
const (
|
|
||||||
Tmpfs PositionalArg = iota
|
|
||||||
Symlink
|
|
||||||
|
|
||||||
Bind
|
|
||||||
BindTry
|
|
||||||
DevBind
|
|
||||||
DevBindTry
|
|
||||||
ROBind
|
|
||||||
ROBindTry
|
|
||||||
|
|
||||||
Chmod
|
|
||||||
Dir
|
|
||||||
RemountRO
|
|
||||||
Procfs
|
|
||||||
DevTmpfs
|
|
||||||
Mqueue
|
|
||||||
|
|
||||||
Perms
|
|
||||||
Size
|
|
||||||
|
|
||||||
OverlaySrc
|
|
||||||
Overlay
|
|
||||||
TmpOverlay
|
|
||||||
ROOverlay
|
|
||||||
|
|
||||||
SyncFd
|
|
||||||
Seccomp
|
|
||||||
|
|
||||||
File
|
|
||||||
BindData
|
|
||||||
ROBindData
|
|
||||||
)
|
|
||||||
|
|
||||||
var positionalArgs = [...]string{
|
|
||||||
Tmpfs: "--tmpfs",
|
|
||||||
Symlink: "--symlink",
|
|
||||||
|
|
||||||
Bind: "--bind",
|
|
||||||
BindTry: "--bind-try",
|
|
||||||
DevBind: "--dev-bind",
|
|
||||||
DevBindTry: "--dev-bind-try",
|
|
||||||
ROBind: "--ro-bind",
|
|
||||||
ROBindTry: "--ro-bind-try",
|
|
||||||
|
|
||||||
Chmod: "--chmod",
|
|
||||||
Dir: "--dir",
|
|
||||||
RemountRO: "--remount-ro",
|
|
||||||
Procfs: "--proc",
|
|
||||||
DevTmpfs: "--dev",
|
|
||||||
Mqueue: "--mqueue",
|
|
||||||
|
|
||||||
Perms: "--perms",
|
|
||||||
Size: "--size",
|
|
||||||
|
|
||||||
OverlaySrc: "--overlay-src",
|
|
||||||
Overlay: "--overlay",
|
|
||||||
TmpOverlay: "--tmp-overlay",
|
|
||||||
ROOverlay: "--ro-overlay",
|
|
||||||
|
|
||||||
SyncFd: "--sync-fd",
|
|
||||||
Seccomp: "--seccomp",
|
|
||||||
|
|
||||||
File: "--file",
|
|
||||||
BindData: "--bind-data",
|
|
||||||
ROBindData: "--ro-bind-data",
|
|
||||||
}
|
|
||||||
|
|
||||||
type PermConfig[T FSBuilder] struct {
|
|
||||||
// set permissions of next argument
|
|
||||||
// (--perms OCTAL)
|
|
||||||
Mode *os.FileMode `json:"mode,omitempty"`
|
|
||||||
// path to get the new permission
|
|
||||||
// (--bind-data, --file, etc.)
|
|
||||||
Inner T `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermConfig[T]) Path() string { return p.Inner.Path() }
|
|
||||||
|
|
||||||
func (p *PermConfig[T]) Len() int {
|
|
||||||
if p.Mode != nil {
|
|
||||||
return p.Inner.Len() + 2
|
|
||||||
} else {
|
|
||||||
return p.Inner.Len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermConfig[T]) Append(args *[]string) {
|
|
||||||
if p.Mode != nil {
|
|
||||||
*args = append(*args, Perms.String(), strconv.FormatInt(int64(*p.Mode), 8))
|
|
||||||
}
|
|
||||||
p.Inner.Append(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TmpfsConfig struct {
|
|
||||||
// set size of tmpfs
|
|
||||||
// (--size BYTES)
|
|
||||||
Size int `json:"size,omitempty"`
|
|
||||||
// mount point of new tmpfs
|
|
||||||
// (--tmpfs DEST)
|
|
||||||
Dir string `json:"dir"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TmpfsConfig) Path() string { return t.Dir }
|
|
||||||
|
|
||||||
func (t *TmpfsConfig) Len() int {
|
|
||||||
if t.Size > 0 {
|
|
||||||
return 4
|
|
||||||
} else {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TmpfsConfig) Append(args *[]string) {
|
|
||||||
if t.Size > 0 {
|
|
||||||
*args = append(*args, Size.String(), strconv.Itoa(t.Size))
|
|
||||||
}
|
|
||||||
*args = append(*args, Tmpfs.String(), t.Dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
type OverlayConfig struct {
|
|
||||||
/*
|
|
||||||
read files from SRC in the following overlay
|
|
||||||
(--overlay-src SRC)
|
|
||||||
*/
|
|
||||||
Src []string `json:"src,omitempty"`
|
|
||||||
|
|
||||||
/*
|
|
||||||
mount overlayfs on DEST, with RWSRC as the host path for writes and
|
|
||||||
WORKDIR an empty directory on the same filesystem as RWSRC
|
|
||||||
(--overlay RWSRC WORKDIR DEST)
|
|
||||||
|
|
||||||
if nil, mount overlayfs on DEST, with writes going to an invisible tmpfs
|
|
||||||
(--tmp-overlay DEST)
|
|
||||||
|
|
||||||
if either strings are empty, mount overlayfs read-only on DEST
|
|
||||||
(--ro-overlay DEST)
|
|
||||||
*/
|
|
||||||
Persist *[2]string `json:"persist,omitempty"`
|
|
||||||
|
|
||||||
/*
|
|
||||||
--overlay RWSRC WORKDIR DEST
|
|
||||||
|
|
||||||
--tmp-overlay DEST
|
|
||||||
|
|
||||||
--ro-overlay DEST
|
|
||||||
*/
|
|
||||||
Dest string `json:"dest"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OverlayConfig) Path() string { return o.Dest }
|
|
||||||
|
|
||||||
func (o *OverlayConfig) Len() int {
|
|
||||||
// (--tmp-overlay DEST) or (--ro-overlay DEST)
|
|
||||||
p := 2
|
|
||||||
// (--overlay RWSRC WORKDIR DEST)
|
|
||||||
if o.Persist != nil && o.Persist[0] != "" && o.Persist[1] != "" {
|
|
||||||
p = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
return p + len(o.Src)*2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OverlayConfig) Append(args *[]string) {
|
|
||||||
// --overlay-src SRC
|
|
||||||
for _, src := range o.Src {
|
|
||||||
*args = append(*args, OverlaySrc.String(), src)
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.Persist != nil {
|
|
||||||
if o.Persist[0] != "" && o.Persist[1] != "" {
|
|
||||||
// --overlay RWSRC WORKDIR
|
|
||||||
*args = append(*args, Overlay.String(), o.Persist[0], o.Persist[1])
|
|
||||||
} else {
|
|
||||||
// --ro-overlay
|
|
||||||
*args = append(*args, ROOverlay.String())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// --tmp-overlay
|
|
||||||
*args = append(*args, TmpOverlay.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEST
|
|
||||||
*args = append(*args, o.Dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SymlinkConfig [2]string
|
|
||||||
|
|
||||||
func (s SymlinkConfig) Path() string { return s[1] }
|
|
||||||
func (s SymlinkConfig) Len() int { return 3 }
|
|
||||||
func (s SymlinkConfig) Append(args *[]string) { *args = append(*args, Symlink.String(), s[0], s[1]) }
|
|
||||||
|
|
||||||
type ChmodConfig map[string]os.FileMode
|
|
||||||
|
|
||||||
func (c ChmodConfig) Len() int { return len(c) }
|
|
||||||
func (c ChmodConfig) Append(args *[]string) {
|
|
||||||
for path, mode := range c {
|
|
||||||
*args = append(*args, Chmod.String(), strconv.FormatInt(int64(mode), 8), path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
DataWrite = iota
|
|
||||||
DataBind
|
|
||||||
DataROBind
|
|
||||||
)
|
|
||||||
|
|
||||||
type DataConfig struct {
|
|
||||||
Dest string `json:"dest"`
|
|
||||||
Data []byte `json:"data,omitempty"`
|
|
||||||
Type int `json:"type"`
|
|
||||||
proc.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DataConfig) Path() string { return d.Dest }
|
|
||||||
func (d *DataConfig) Len() int {
|
|
||||||
if d == nil || d.Data == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
func (d *DataConfig) Init(fd uintptr, v **os.File) uintptr {
|
|
||||||
if d.File != nil {
|
|
||||||
panic("file initialised twice")
|
|
||||||
}
|
|
||||||
d.File = proc.NewWriterTo(d)
|
|
||||||
return d.File.Init(fd, v)
|
|
||||||
}
|
|
||||||
func (d *DataConfig) WriteTo(w io.Writer) (int64, error) {
|
|
||||||
n, err := w.Write(d.Data)
|
|
||||||
return int64(n), err
|
|
||||||
}
|
|
||||||
func (d *DataConfig) Append(args *[]string) {
|
|
||||||
if d == nil || d.Data == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var a PositionalArg
|
|
||||||
switch d.Type {
|
|
||||||
case DataWrite:
|
|
||||||
a = File
|
|
||||||
case DataBind:
|
|
||||||
a = BindData
|
|
||||||
case DataROBind:
|
|
||||||
a = ROBindData
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("invalid type %d", a))
|
|
||||||
}
|
|
||||||
|
|
||||||
*args = append(*args, a.String(), strconv.Itoa(int(d.Fd())), d.Dest)
|
|
||||||
}
|
|
@ -1,249 +0,0 @@
|
|||||||
package bwrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
static boolean args
|
|
||||||
*/
|
|
||||||
|
|
||||||
type BoolArg int
|
|
||||||
|
|
||||||
func (b BoolArg) Unwrap() []string {
|
|
||||||
return boolArgs[b]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
UnshareAll BoolArg = iota
|
|
||||||
UnshareUser
|
|
||||||
UnshareIPC
|
|
||||||
UnsharePID
|
|
||||||
UnshareNet
|
|
||||||
UnshareUTS
|
|
||||||
UnshareCGroup
|
|
||||||
ShareNet
|
|
||||||
|
|
||||||
UserNS
|
|
||||||
Clearenv
|
|
||||||
|
|
||||||
NewSession
|
|
||||||
DieWithParent
|
|
||||||
AsInit
|
|
||||||
)
|
|
||||||
|
|
||||||
var boolArgs = [...][]string{
|
|
||||||
UnshareAll: {"--unshare-all", "--unshare-user"},
|
|
||||||
UnshareUser: {"--unshare-user"},
|
|
||||||
UnshareIPC: {"--unshare-ipc"},
|
|
||||||
UnsharePID: {"--unshare-pid"},
|
|
||||||
UnshareNet: {"--unshare-net"},
|
|
||||||
UnshareUTS: {"--unshare-uts"},
|
|
||||||
UnshareCGroup: {"--unshare-cgroup"},
|
|
||||||
ShareNet: {"--share-net"},
|
|
||||||
|
|
||||||
UserNS: {"--disable-userns", "--assert-userns-disabled"},
|
|
||||||
Clearenv: {"--clearenv"},
|
|
||||||
|
|
||||||
NewSession: {"--new-session"},
|
|
||||||
DieWithParent: {"--die-with-parent"},
|
|
||||||
AsInit: {"--as-pid-1"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) boolArgs() Builder {
|
|
||||||
b := boolArg{
|
|
||||||
UserNS: !c.UserNS,
|
|
||||||
Clearenv: c.Clearenv,
|
|
||||||
|
|
||||||
NewSession: c.NewSession,
|
|
||||||
DieWithParent: c.DieWithParent,
|
|
||||||
AsInit: c.AsInit,
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Unshare == nil {
|
|
||||||
b[UnshareAll] = true
|
|
||||||
b[ShareNet] = c.Net
|
|
||||||
} else {
|
|
||||||
b[UnshareUser] = c.Unshare.User
|
|
||||||
b[UnshareIPC] = c.Unshare.IPC
|
|
||||||
b[UnsharePID] = c.Unshare.PID
|
|
||||||
b[UnshareNet] = c.Unshare.Net
|
|
||||||
b[UnshareUTS] = c.Unshare.UTS
|
|
||||||
b[UnshareCGroup] = c.Unshare.CGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
return &b
|
|
||||||
}
|
|
||||||
|
|
||||||
type boolArg [len(boolArgs)]bool
|
|
||||||
|
|
||||||
func (b *boolArg) Len() (l int) {
|
|
||||||
for i, v := range b {
|
|
||||||
if v {
|
|
||||||
l += len(boolArgs[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *boolArg) Append(args *[]string) {
|
|
||||||
for i, v := range b {
|
|
||||||
if v {
|
|
||||||
*args = append(*args, BoolArg(i).Unwrap()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
static integer args
|
|
||||||
*/
|
|
||||||
|
|
||||||
type IntArg int
|
|
||||||
|
|
||||||
func (i IntArg) Unwrap() string {
|
|
||||||
return intArgs[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
UID IntArg = iota
|
|
||||||
GID
|
|
||||||
)
|
|
||||||
|
|
||||||
var intArgs = [...]string{
|
|
||||||
UID: "--uid",
|
|
||||||
GID: "--gid",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) intArgs() Builder {
|
|
||||||
return &intArg{
|
|
||||||
UID: c.UID,
|
|
||||||
GID: c.GID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type intArg [len(intArgs)]*int
|
|
||||||
|
|
||||||
func (n *intArg) Len() (l int) {
|
|
||||||
for _, v := range n {
|
|
||||||
if v != nil {
|
|
||||||
l += 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *intArg) Append(args *[]string) {
|
|
||||||
for i, v := range n {
|
|
||||||
if v != nil {
|
|
||||||
*args = append(*args, IntArg(i).Unwrap(), strconv.Itoa(*v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
static string args
|
|
||||||
*/
|
|
||||||
|
|
||||||
type StringArg int
|
|
||||||
|
|
||||||
func (s StringArg) Unwrap() string {
|
|
||||||
return stringArgs[s]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
Hostname StringArg = iota
|
|
||||||
Chdir
|
|
||||||
UnsetEnv
|
|
||||||
LockFile
|
|
||||||
)
|
|
||||||
|
|
||||||
var stringArgs = [...]string{
|
|
||||||
Hostname: "--hostname",
|
|
||||||
Chdir: "--chdir",
|
|
||||||
UnsetEnv: "--unsetenv",
|
|
||||||
LockFile: "--lock-file",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) stringArgs() Builder {
|
|
||||||
n := stringArg{
|
|
||||||
UnsetEnv: c.UnsetEnv,
|
|
||||||
LockFile: c.LockFile,
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Hostname != "" {
|
|
||||||
n[Hostname] = []string{c.Hostname}
|
|
||||||
}
|
|
||||||
if c.Chdir != "" {
|
|
||||||
n[Chdir] = []string{c.Chdir}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &n
|
|
||||||
}
|
|
||||||
|
|
||||||
type stringArg [len(stringArgs)][]string
|
|
||||||
|
|
||||||
func (s *stringArg) Len() (l int) {
|
|
||||||
for _, arg := range s {
|
|
||||||
l += len(arg) * 2
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stringArg) Append(args *[]string) {
|
|
||||||
for i, arg := range s {
|
|
||||||
for _, v := range arg {
|
|
||||||
*args = append(*args, StringArg(i).Unwrap(), v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
static pair args
|
|
||||||
*/
|
|
||||||
|
|
||||||
type PairArg int
|
|
||||||
|
|
||||||
func (p PairArg) Unwrap() string {
|
|
||||||
return pairArgs[p]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
SetEnv PairArg = iota
|
|
||||||
)
|
|
||||||
|
|
||||||
var pairArgs = [...]string{
|
|
||||||
SetEnv: "--setenv",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) pairArgs() Builder {
|
|
||||||
var n pairArg
|
|
||||||
n[SetEnv] = make([][2]string, len(c.SetEnv))
|
|
||||||
keys := make([]string, 0, len(c.SetEnv))
|
|
||||||
for k := range c.SetEnv {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
slices.Sort(keys)
|
|
||||||
for i, k := range keys {
|
|
||||||
n[SetEnv][i] = [2]string{k, c.SetEnv[k]}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &n
|
|
||||||
}
|
|
||||||
|
|
||||||
type pairArg [len(pairArgs)][][2]string
|
|
||||||
|
|
||||||
func (p *pairArg) Len() (l int) {
|
|
||||||
for _, v := range p {
|
|
||||||
l += len(v) * 3
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pairArg) Append(args *[]string) {
|
|
||||||
for i, arg := range p {
|
|
||||||
for _, v := range arg {
|
|
||||||
*args = append(*args, PairArg(i).Unwrap(), v[0], v[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
package bwrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/gob"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
gob.Register(new(pairF))
|
|
||||||
gob.Register(new(stringF))
|
|
||||||
}
|
|
||||||
|
|
||||||
type pairF [3]string
|
|
||||||
|
|
||||||
func (p *pairF) Path() string { return p[2] }
|
|
||||||
func (p *pairF) Len() int { return len(p) }
|
|
||||||
func (p *pairF) Append(args *[]string) { *args = append(*args, p[0], p[1], p[2]) }
|
|
||||||
|
|
||||||
type stringF [2]string
|
|
||||||
|
|
||||||
func (s stringF) Path() string { return s[1] }
|
|
||||||
func (s stringF) Len() int { return len(s) /* compiler replaces this with 2 */ }
|
|
||||||
func (s stringF) Append(args *[]string) { *args = append(*args, s[0], s[1]) }
|
|
||||||
|
|
||||||
func newFile(name string, f *os.File) FDBuilder { return &fileF{name: name, file: f} }
|
|
||||||
|
|
||||||
type fileF struct {
|
|
||||||
name string
|
|
||||||
file *os.File
|
|
||||||
proc.BaseFile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fileF) ErrCount() int { return 0 }
|
|
||||||
func (f *fileF) Fulfill(_ context.Context, _ func(error)) error { f.Set(f.file); return nil }
|
|
||||||
|
|
||||||
func (f *fileF) Len() int {
|
|
||||||
if f.file == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fileF) Append(args *[]string) {
|
|
||||||
if f.file == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*args = append(*args, f.name, strconv.Itoa(int(f.Fd())))
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
package helper_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBwrap(t *testing.T) {
|
|
||||||
sc := &bwrap.Config{
|
|
||||||
Net: true,
|
|
||||||
Hostname: "localhost",
|
|
||||||
Chdir: "/nonexistent",
|
|
||||||
Clearenv: true,
|
|
||||||
NewSession: true,
|
|
||||||
DieWithParent: true,
|
|
||||||
AsInit: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("nonexistent bwrap name", func(t *testing.T) {
|
|
||||||
bubblewrapName := helper.BubblewrapName
|
|
||||||
helper.BubblewrapName = "/nonexistent"
|
|
||||||
t.Cleanup(func() {
|
|
||||||
helper.BubblewrapName = bubblewrapName
|
|
||||||
})
|
|
||||||
|
|
||||||
h := helper.MustNewBwrap(
|
|
||||||
sc, "fortify",
|
|
||||||
argsWt, argF,
|
|
||||||
nil, nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
|
|
||||||
t.Errorf("Start: error = %v, wantErr %v",
|
|
||||||
err, os.ErrNotExist)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
|
||||||
if got := helper.MustNewBwrap(
|
|
||||||
sc, "fortify",
|
|
||||||
argsWt, argF,
|
|
||||||
nil, nil,
|
|
||||||
); got == nil {
|
|
||||||
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
|
|
||||||
sc, argsWt, "fortify")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid bwrap config new helper panic", func(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
wantPanic := "argument contains null character"
|
|
||||||
if r := recover(); r != wantPanic {
|
|
||||||
t.Errorf("MustNewBwrap: panic = %q, want %q",
|
|
||||||
r, wantPanic)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
helper.MustNewBwrap(
|
|
||||||
&bwrap.Config{Hostname: "\x00"}, "fortify",
|
|
||||||
nil, argF,
|
|
||||||
nil, nil,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("start without pipes", func(t *testing.T) {
|
|
||||||
helper.InternalReplaceExecCommand(t)
|
|
||||||
|
|
||||||
h := helper.MustNewBwrap(
|
|
||||||
sc, "crash-test-dummy",
|
|
||||||
nil, argFChecked,
|
|
||||||
nil, nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
|
||||||
h.Stdout(stdout).Stderr(stderr)
|
|
||||||
|
|
||||||
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := h.Start(c, false); err != nil {
|
|
||||||
t.Errorf("Start: error = %v",
|
|
||||||
err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.Wait(); err != nil {
|
|
||||||
t.Errorf("Wait() err = %v stderr = %s",
|
|
||||||
err, stderr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("implementation compliance", func(t *testing.T) {
|
|
||||||
testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil, nil) })
|
|
||||||
})
|
|
||||||
}
|
|
84
helper/cmd.go
Normal file
84
helper/cmd.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/helper/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
||||||
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
|
func NewDirect(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(cmd *exec.Cmd),
|
||||||
|
extraFiles []*os.File,
|
||||||
|
) Helper {
|
||||||
|
d, args := newHelperCmd(ctx, name, wt, stat, argF, extraFiles)
|
||||||
|
d.Args = append(d.Args, args...)
|
||||||
|
if cmdF != nil {
|
||||||
|
cmdF(d.Cmd)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHelperCmd(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
extraFiles []*os.File,
|
||||||
|
) (cmd *helperCmd, args []string) {
|
||||||
|
cmd = new(helperCmd)
|
||||||
|
cmd.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
|
||||||
|
cmd.Cmd = exec.CommandContext(ctx, name)
|
||||||
|
cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
||||||
|
cmd.WaitDelay = WaitDelay
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// helperCmd provides a [exec.Cmd] wrapper around helper ipc.
|
||||||
|
type helperCmd struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
*helperFiles
|
||||||
|
*exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helperCmd) Start() error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
// Check for doubled Start calls before we defer failure cleanup. If the prior
|
||||||
|
// call to Start succeeded, we don't want to spuriously close its pipes.
|
||||||
|
if h.Cmd != nil && h.Cmd.Process != nil {
|
||||||
|
return errors.New("helper: already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Env = slices.Grow(h.Env, 2)
|
||||||
|
if h.useArgsFd {
|
||||||
|
h.Env = append(h.Env, HakureiHelper+"=1")
|
||||||
|
} else {
|
||||||
|
h.Env = append(h.Env, HakureiHelper+"=0")
|
||||||
|
}
|
||||||
|
if h.useStatFd {
|
||||||
|
h.Env = append(h.Env, HakureiStatus+"=1")
|
||||||
|
|
||||||
|
// stat is populated on fulfill
|
||||||
|
h.Cancel = func() error { return h.stat.Close() }
|
||||||
|
} else {
|
||||||
|
h.Env = append(h.Env, HakureiStatus+"=0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles)
|
||||||
|
}
|
39
helper/cmd_test.go
Normal file
39
helper/cmd_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package helper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCmd(t *testing.T) {
|
||||||
|
t.Run("start non-existent helper path", func(t *testing.T) {
|
||||||
|
h := helper.NewDirect(t.Context(), "/proc/nonexistent", argsWt, false, argF, nil, nil)
|
||||||
|
|
||||||
|
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("Start: error = %v, wantErr %v",
|
||||||
|
err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||||
|
if got := helper.NewDirect(t.Context(), "hakurei", argsWt, false, argF, nil, nil); got == nil {
|
||||||
|
t.Errorf("NewDirect(%q, %q) got nil",
|
||||||
|
argsWt, "hakurei")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("implementation compliance", func(t *testing.T) {
|
||||||
|
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||||
|
return helper.NewDirect(ctx, os.Args[0], argsWt, stat, argF, func(cmd *exec.Cmd) {
|
||||||
|
setOutput(&cmd.Stdout, &cmd.Stderr)
|
||||||
|
}, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
76
helper/container.go
Normal file
76
helper/container.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/helper/proc"
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New initialises a Helper instance with wt as the null-terminated argument writer.
|
||||||
|
func New(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(container *sandbox.Container),
|
||||||
|
extraFiles []*os.File,
|
||||||
|
) Helper {
|
||||||
|
var args []string
|
||||||
|
h := new(helperContainer)
|
||||||
|
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
|
||||||
|
h.Container = sandbox.New(ctx, name, args...)
|
||||||
|
h.WaitDelay = WaitDelay
|
||||||
|
if cmdF != nil {
|
||||||
|
cmdF(h.Container)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// helperContainer provides a [sandbox.Container] wrapper around helper ipc.
|
||||||
|
type helperContainer struct {
|
||||||
|
started bool
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
*helperFiles
|
||||||
|
*sandbox.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helperContainer) Start() error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
if h.started {
|
||||||
|
return errors.New("helper: already started")
|
||||||
|
}
|
||||||
|
h.started = true
|
||||||
|
|
||||||
|
h.Env = slices.Grow(h.Env, 2)
|
||||||
|
if h.useArgsFd {
|
||||||
|
h.Env = append(h.Env, HakureiHelper+"=1")
|
||||||
|
} else {
|
||||||
|
h.Env = append(h.Env, HakureiHelper+"=0")
|
||||||
|
}
|
||||||
|
if h.useStatFd {
|
||||||
|
h.Env = append(h.Env, HakureiStatus+"=1")
|
||||||
|
|
||||||
|
// stat is populated on fulfill
|
||||||
|
h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
|
||||||
|
} else {
|
||||||
|
h.Env = append(h.Env, HakureiStatus+"=0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, func() error {
|
||||||
|
if err := h.Container.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return h.Container.Serve()
|
||||||
|
}, h.files, h.extraFiles)
|
||||||
|
}
|
57
helper/container_test.go
Normal file
57
helper/container_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package helper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
|
"git.gensokyo.uk/security/hakurei/internal"
|
||||||
|
"git.gensokyo.uk/security/hakurei/internal/hlog"
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContainer(t *testing.T) {
|
||||||
|
t.Run("start empty container", func(t *testing.T) {
|
||||||
|
h := helper.New(t.Context(), "/nonexistent", argsWt, false, argF, nil, nil)
|
||||||
|
|
||||||
|
wantErr := "sandbox: starting an empty container"
|
||||||
|
if err := h.Start(); err == nil || err.Error() != wantErr {
|
||||||
|
t.Errorf("Start: error = %v, wantErr %q",
|
||||||
|
err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||||
|
if got := helper.New(t.Context(), "hakurei", argsWt, false, argF, nil, nil); got == nil {
|
||||||
|
t.Errorf("New(%q, %q) got nil",
|
||||||
|
argsWt, "hakurei")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("implementation compliance", func(t *testing.T) {
|
||||||
|
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||||
|
return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(container *sandbox.Container) {
|
||||||
|
setOutput(&container.Stdout, &container.Stderr)
|
||||||
|
container.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
|
||||||
|
return exec.CommandContext(ctx, os.Args[0], "-test.v",
|
||||||
|
"-test.run=TestHelperInit", "--", "init")
|
||||||
|
}
|
||||||
|
container.Bind("/", "/", 0)
|
||||||
|
container.Proc("/proc")
|
||||||
|
container.Dev("/dev")
|
||||||
|
}, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperInit(t *testing.T) {
|
||||||
|
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sandbox.SetOutput(hlog.Output{})
|
||||||
|
sandbox.Init(hlog.Prepare, func(bool) { internal.InstallFmsg(false) })
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// direct wraps *exec.Cmd and manages status and args fd.
|
|
||||||
// Args is always 3 and status if set is always 4.
|
|
||||||
type direct struct {
|
|
||||||
lock sync.RWMutex
|
|
||||||
*helperCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *direct) Start(ctx context.Context, stat bool) error {
|
|
||||||
h.lock.Lock()
|
|
||||||
defer h.lock.Unlock()
|
|
||||||
|
|
||||||
// Check for doubled Start calls before we defer failure cleanup. If the prior
|
|
||||||
// call to Start succeeded, we don't want to spuriously close its pipes.
|
|
||||||
if h.Cmd != nil && h.Cmd.Process != nil {
|
|
||||||
return errors.New("exec: already started")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := h.finalise(ctx, stat)
|
|
||||||
h.Cmd.Args = append(h.Cmd.Args, args...)
|
|
||||||
return proc.Fulfill(ctx, h.Cmd, h.files, h.extraFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
// New initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
|
||||||
// Function argF returns an array of arguments passed directly to the child process.
|
|
||||||
func New(wt io.WriterTo, name string, argF func(argsFd, statFd int) []string) Helper {
|
|
||||||
d := new(direct)
|
|
||||||
d.helperCmd = newHelperCmd(d, name, wt, argF, nil)
|
|
||||||
return d
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package helper_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDirect(t *testing.T) {
|
|
||||||
t.Run("start non-existent helper path", func(t *testing.T) {
|
|
||||||
h := helper.New(argsWt, "/nonexistent", argF)
|
|
||||||
|
|
||||||
if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
|
|
||||||
t.Errorf("Start: error = %v, wantErr %v",
|
|
||||||
err, os.ErrNotExist)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
|
||||||
if got := helper.New(argsWt, "fortify", argF); got == nil {
|
|
||||||
t.Errorf("New(%q, %q) got nil",
|
|
||||||
argsWt, "fortify")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("implementation compliance", func(t *testing.T) {
|
|
||||||
testHelper(t, func() helper.Helper { return helper.New(argsWt, "crash-test-dummy", argF) })
|
|
||||||
})
|
|
||||||
}
|
|
127
helper/helper.go
127
helper/helper.go
@ -1,4 +1,4 @@
|
|||||||
// Package helper runs external helpers with optional sandboxing and manages their status/args pipes.
|
// Package helper runs external helpers with optional sandboxing.
|
||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -6,82 +6,71 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"slices"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
"git.gensokyo.uk/security/hakurei/helper/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var WaitDelay = 2 * time.Second
|
||||||
WaitDelay = 2 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
|
// HakureiHelper is set to 1 when args fd is enabled and 0 otherwise.
|
||||||
FortifyHelper = "FORTIFY_HELPER"
|
HakureiHelper = "HAKUREI_HELPER"
|
||||||
// FortifyStatus is set to 1 when stat fd is enabled and 0 otherwise.
|
// HakureiStatus is set to 1 when stat fd is enabled and 0 otherwise.
|
||||||
FortifyStatus = "FORTIFY_STATUS"
|
HakureiStatus = "HAKUREI_STATUS"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Helper interface {
|
type Helper interface {
|
||||||
// Stdin sets the standard input of Helper.
|
|
||||||
Stdin(r io.Reader) Helper
|
|
||||||
// Stdout sets the standard output of Helper.
|
|
||||||
Stdout(w io.Writer) Helper
|
|
||||||
// Stderr sets the standard error of Helper.
|
|
||||||
Stderr(w io.Writer) Helper
|
|
||||||
// SetEnv sets the environment of Helper.
|
|
||||||
SetEnv(env []string) Helper
|
|
||||||
|
|
||||||
// Start starts the helper process.
|
// Start starts the helper process.
|
||||||
// A status pipe is passed to the helper if stat is true.
|
Start() error
|
||||||
Start(ctx context.Context, stat bool) error
|
// Wait blocks until Helper exits.
|
||||||
// Wait blocks until Helper exits and releases all its resources.
|
|
||||||
Wait() error
|
Wait() error
|
||||||
|
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHelperCmd(
|
func newHelperFiles(
|
||||||
h Helper, name string,
|
ctx context.Context,
|
||||||
wt io.WriterTo, argF func(argsFd, statFd int) []string,
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
extraFiles []*os.File,
|
extraFiles []*os.File,
|
||||||
) (cmd *helperCmd) {
|
) (hl *helperFiles, args []string) {
|
||||||
cmd = new(helperCmd)
|
hl = new(helperFiles)
|
||||||
|
hl.ctx = ctx
|
||||||
|
hl.useArgsFd = wt != nil
|
||||||
|
hl.useStatFd = stat
|
||||||
|
|
||||||
cmd.r = h
|
hl.extraFiles = new(proc.ExtraFilesPre)
|
||||||
cmd.name = name
|
|
||||||
|
|
||||||
cmd.extraFiles = new(proc.ExtraFilesPre)
|
|
||||||
for _, f := range extraFiles {
|
for _, f := range extraFiles {
|
||||||
_, v := cmd.extraFiles.Append()
|
_, v := hl.extraFiles.Append()
|
||||||
*v = f
|
*v = f
|
||||||
}
|
}
|
||||||
|
|
||||||
argsFd := -1
|
argsFd := -1
|
||||||
if wt != nil {
|
if hl.useArgsFd {
|
||||||
f := proc.NewWriterTo(wt)
|
f := proc.NewWriterTo(wt)
|
||||||
argsFd = int(proc.InitFile(f, cmd.extraFiles))
|
argsFd = int(proc.InitFile(f, hl.extraFiles))
|
||||||
cmd.files = append(cmd.files, f)
|
hl.files = append(hl.files, f)
|
||||||
cmd.hasArgsFd = true
|
|
||||||
}
|
}
|
||||||
cmd.argF = func(statFd int) []string { return argF(argsFd, statFd) }
|
|
||||||
|
|
||||||
|
statFd := -1
|
||||||
|
if hl.useStatFd {
|
||||||
|
f := proc.NewStat(&hl.stat)
|
||||||
|
statFd = int(proc.InitFile(f, hl.extraFiles))
|
||||||
|
hl.files = append(hl.files, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = argF(argsFd, statFd)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// helperCmd wraps Cmd and implements methods shared across all Helper implementations.
|
// helperFiles provides a generic wrapper around helper ipc.
|
||||||
type helperCmd struct {
|
type helperFiles struct {
|
||||||
// ref to parent
|
|
||||||
r Helper
|
|
||||||
|
|
||||||
// returns an array of arguments passed directly
|
|
||||||
// to the helper process
|
|
||||||
argF func(statFd int) []string
|
|
||||||
// whether argsFd is present
|
// whether argsFd is present
|
||||||
hasArgsFd bool
|
useArgsFd bool
|
||||||
|
// whether statFd is present
|
||||||
|
useStatFd bool
|
||||||
|
|
||||||
// closes statFd
|
// closes statFd
|
||||||
stat io.Closer
|
stat io.Closer
|
||||||
@ -90,45 +79,5 @@ type helperCmd struct {
|
|||||||
// passed through to [proc.Fulfill] and [proc.InitFile]
|
// passed through to [proc.Fulfill] and [proc.InitFile]
|
||||||
extraFiles *proc.ExtraFilesPre
|
extraFiles *proc.ExtraFilesPre
|
||||||
|
|
||||||
name string
|
ctx context.Context
|
||||||
stdin io.Reader
|
|
||||||
stdout, stderr io.Writer
|
|
||||||
env []string
|
|
||||||
*exec.Cmd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *helperCmd) Stdin(r io.Reader) Helper { h.stdin = r; return h.r }
|
|
||||||
func (h *helperCmd) Stdout(w io.Writer) Helper { h.stdout = w; return h.r }
|
|
||||||
func (h *helperCmd) Stderr(w io.Writer) Helper { h.stderr = w; return h.r }
|
|
||||||
func (h *helperCmd) SetEnv(env []string) Helper { h.env = env; return h.r }
|
|
||||||
|
|
||||||
// finalise initialises the underlying [exec.Cmd] object.
|
|
||||||
func (h *helperCmd) finalise(ctx context.Context, stat bool) (args []string) {
|
|
||||||
h.Cmd = commandContext(ctx, h.name)
|
|
||||||
h.Cmd.Stdin, h.Cmd.Stdout, h.Cmd.Stderr = h.stdin, h.stdout, h.stderr
|
|
||||||
h.Cmd.Env = slices.Grow(h.env, 2)
|
|
||||||
if h.hasArgsFd {
|
|
||||||
h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=1")
|
|
||||||
} else {
|
|
||||||
h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=0")
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Cmd.Cancel = func() error { return h.Cmd.Process.Signal(syscall.SIGTERM) }
|
|
||||||
h.Cmd.WaitDelay = WaitDelay
|
|
||||||
|
|
||||||
statFd := -1
|
|
||||||
if stat {
|
|
||||||
f := proc.NewStat(&h.stat)
|
|
||||||
statFd = int(proc.InitFile(f, h.extraFiles))
|
|
||||||
h.files = append(h.files, f)
|
|
||||||
h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=1")
|
|
||||||
|
|
||||||
// stat is populated on fulfill
|
|
||||||
h.Cmd.Cancel = func() error { return h.stat.Close() }
|
|
||||||
} else {
|
|
||||||
h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=0")
|
|
||||||
}
|
|
||||||
return h.argF(statFd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var commandContext = exec.CommandContext
|
|
||||||
|
@ -4,18 +4,20 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
wantArgs = []string{
|
wantArgs = []string{
|
||||||
"unix:path=/run/dbus/system_bus_socket",
|
"unix:path=/run/dbus/system_bus_socket",
|
||||||
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
|
"/tmp/hakurei.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.bluez",
|
"--talk=org.bluez",
|
||||||
"--talk=org.freedesktop.Avahi",
|
"--talk=org.freedesktop.Avahi",
|
||||||
@ -35,7 +37,8 @@ func argF(argsFd, statFd int) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func argFChecked(argsFd, statFd int) (args []string) {
|
func argFChecked(argsFd, statFd int) (args []string) {
|
||||||
args = make([]string, 0, 4)
|
args = make([]string, 0, 6)
|
||||||
|
args = append(args, "-test.run=TestHelperStub", "--")
|
||||||
if argsFd > -1 {
|
if argsFd > -1 {
|
||||||
args = append(args, "--args", strconv.Itoa(argsFd))
|
args = append(args, "--args", strconv.Itoa(argsFd))
|
||||||
}
|
}
|
||||||
@ -46,14 +49,15 @@ func argFChecked(argsFd, statFd int) (args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this function tests an implementation of the helper.Helper interface
|
// this function tests an implementation of the helper.Helper interface
|
||||||
func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
|
||||||
helper.InternalReplaceExecCommand(t)
|
oldWaitDelay := helper.WaitDelay
|
||||||
|
helper.WaitDelay = 16 * time.Second
|
||||||
|
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
||||||
|
|
||||||
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
||||||
h := createHelper()
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
|
stdout := new(strings.Builder)
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
|
||||||
h.Stdout(stdout).Stderr(stderr)
|
|
||||||
|
|
||||||
t.Run("wait not yet started helper", func(t *testing.T) {
|
t.Run("wait not yet started helper", func(t *testing.T) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -65,10 +69,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
|||||||
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
|
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
|
|
||||||
t.Log("starting helper stub")
|
t.Log("starting helper stub")
|
||||||
if err := h.Start(ctx, true); err != nil {
|
if err := h.Start(); err != nil {
|
||||||
t.Errorf("Start: error = %v", err)
|
t.Errorf("Start: error = %v", err)
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
@ -77,8 +79,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
|||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
t.Run("start already started helper", func(t *testing.T) {
|
t.Run("start already started helper", func(t *testing.T) {
|
||||||
wantErr := "exec: already started"
|
wantErr := "helper: already started"
|
||||||
if err := h.Start(ctx, true); err != nil && err.Error() != wantErr {
|
if err := h.Start(); err != nil && err.Error() != wantErr {
|
||||||
t.Errorf("Start: error = %v, wantErr %v",
|
t.Errorf("Start: error = %v, wantErr %v",
|
||||||
err, wantErr)
|
err, wantErr)
|
||||||
return
|
return
|
||||||
@ -87,8 +89,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
|||||||
|
|
||||||
t.Log("waiting on helper")
|
t.Log("waiting on helper")
|
||||||
if err := h.Wait(); !errors.Is(err, context.Canceled) {
|
if err := h.Wait(); !errors.Is(err, context.Canceled) {
|
||||||
t.Errorf("Wait() err = %v stderr = %s",
|
t.Errorf("Wait: error = %v",
|
||||||
err, stderr)
|
err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("wait already finalised helper", func(t *testing.T) {
|
t.Run("wait already finalised helper", func(t *testing.T) {
|
||||||
@ -100,34 +102,36 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
|
if got := trimStdout(stdout); got != wantPayload {
|
||||||
t.Errorf("Start: stdout = %v, want %v",
|
t.Errorf("Start: stdout = %q, want %q",
|
||||||
got, wantPayload)
|
got, wantPayload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("start helper and wait", func(t *testing.T) {
|
t.Run("start helper and wait", func(t *testing.T) {
|
||||||
h := createHelper()
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
|
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
|
||||||
h.Stdout(stdout).Stderr(stderr)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
stdout := new(strings.Builder)
|
||||||
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
|
||||||
|
|
||||||
if err := h.Start(ctx, false); err != nil {
|
if err := h.Start(); err != nil {
|
||||||
t.Errorf("Start() error = %v",
|
t.Errorf("Start: error = %v",
|
||||||
err)
|
err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.Wait(); err != nil {
|
if err := h.Wait(); err != nil {
|
||||||
t.Errorf("Wait() err = %v stdout = %s stderr = %s",
|
t.Errorf("Wait: error = %v stdout = %q",
|
||||||
err, stdout, stderr)
|
err, stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
|
if got := trimStdout(stdout); got != wantPayload {
|
||||||
t.Errorf("Start() stdout = %v, want %v",
|
t.Errorf("Start: stdout = %q, want %q",
|
||||||
got, wantPayload)
|
got, wantPayload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trimStdout(stdout fmt.Stringer) string {
|
||||||
|
return strings.TrimPrefix(stdout.String(), "=== RUN TestHelperInit\n")
|
||||||
|
}
|
||||||
|
@ -60,7 +60,10 @@ func (f *ExtraFilesPre) copy(e []*os.File) []*os.File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
|
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
|
||||||
func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *ExtraFilesPre) (err error) {
|
func Fulfill(ctx context.Context,
|
||||||
|
v *[]*os.File, start func() error,
|
||||||
|
files []File, extraFiles *ExtraFilesPre,
|
||||||
|
) (err error) {
|
||||||
var ecs int
|
var ecs int
|
||||||
for _, o := range files {
|
for _, o := range files {
|
||||||
ecs += o.ErrCount()
|
ecs += o.ErrCount()
|
||||||
@ -77,8 +80,8 @@ func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *Extra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.ExtraFiles = extraFiles.Files()
|
*v = extraFiles.Files()
|
||||||
if err = cmd.Start(); err != nil {
|
if err = start(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
|
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
|
||||||
@ -25,13 +26,20 @@ func (f *writeToFile) Fulfill(ctx context.Context, dispatchErr func(error)) erro
|
|||||||
f.Set(r)
|
f.Set(r)
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() { _, err = f.wt.WriteTo(w); dispatchErr(err); dispatchErr(w.Close()); close(done) }()
|
go func() {
|
||||||
|
_, err = f.wt.WriteTo(w)
|
||||||
|
dispatchErr(err)
|
||||||
|
dispatchErr(w.Close())
|
||||||
|
close(done)
|
||||||
|
runtime.KeepAlive(r)
|
||||||
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
dispatchErr(nil)
|
dispatchErr(nil)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
dispatchErr(w.Close()) // this aborts WriteTo with file already closed
|
dispatchErr(w.Close()) // this aborts WriteTo with file already closed
|
||||||
|
runtime.KeepAlive(r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -83,6 +91,7 @@ func (f *statFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
|
|||||||
default:
|
default:
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
runtime.KeepAlive(w)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@ -91,6 +100,7 @@ func (f *statFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
|
|||||||
dispatchErr(nil)
|
dispatchErr(nil)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
dispatchErr(r.Close()) // this aborts Read with file already closed
|
dispatchErr(r.Close()) // this aborts Read with file already closed
|
||||||
|
runtime.KeepAlive(w)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -1,300 +0,0 @@
|
|||||||
#ifndef _GNU_SOURCE
|
|
||||||
#define _GNU_SOURCE // CLONE_NEWUSER
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "seccomp-export.h"
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <assert.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <sys/syscall.h>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <sys/ioctl.h>
|
|
||||||
#include <sys/personality.h>
|
|
||||||
#include <sched.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
|
|
||||||
|
|
||||||
struct f_syscall_act {
|
|
||||||
int syscall;
|
|
||||||
int m_errno;
|
|
||||||
struct scmp_arg_cmp *arg;
|
|
||||||
};
|
|
||||||
|
|
||||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
|
||||||
|
|
||||||
#define SECCOMP_RULESET_ADD(ruleset) do { \
|
|
||||||
if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\""); \
|
|
||||||
for (int i = 0; i < LEN(ruleset); i++) { \
|
|
||||||
assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \
|
|
||||||
\
|
|
||||||
if (ruleset[i].arg) \
|
|
||||||
ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \
|
|
||||||
else \
|
|
||||||
ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0); \
|
|
||||||
\
|
|
||||||
if (ret == -EFAULT) { \
|
|
||||||
res = 4; \
|
|
||||||
goto out; \
|
|
||||||
} else if (ret < 0) { \
|
|
||||||
res = 5; \
|
|
||||||
errno = -ret; \
|
|
||||||
goto out; \
|
|
||||||
} \
|
|
||||||
} \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) {
|
|
||||||
int32_t res = 0; // refer to resErr for meaning
|
|
||||||
int allow_multiarch = opts & F_MULTIARCH;
|
|
||||||
int allowed_personality = PER_LINUX;
|
|
||||||
|
|
||||||
if (opts & F_LINUX32)
|
|
||||||
allowed_personality = PER_LINUX32;
|
|
||||||
|
|
||||||
// flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532
|
|
||||||
|
|
||||||
struct f_syscall_act deny_common[] = {
|
|
||||||
// Block dmesg
|
|
||||||
{SCMP_SYS(syslog), EPERM},
|
|
||||||
// Useless old syscall
|
|
||||||
{SCMP_SYS(uselib), EPERM},
|
|
||||||
// Don't allow disabling accounting
|
|
||||||
{SCMP_SYS(acct), EPERM},
|
|
||||||
// Don't allow reading current quota use
|
|
||||||
{SCMP_SYS(quotactl), EPERM},
|
|
||||||
|
|
||||||
// Don't allow access to the kernel keyring
|
|
||||||
{SCMP_SYS(add_key), EPERM},
|
|
||||||
{SCMP_SYS(keyctl), EPERM},
|
|
||||||
{SCMP_SYS(request_key), EPERM},
|
|
||||||
|
|
||||||
// Scary VM/NUMA ops
|
|
||||||
{SCMP_SYS(move_pages), EPERM},
|
|
||||||
{SCMP_SYS(mbind), EPERM},
|
|
||||||
{SCMP_SYS(get_mempolicy), EPERM},
|
|
||||||
{SCMP_SYS(set_mempolicy), EPERM},
|
|
||||||
{SCMP_SYS(migrate_pages), EPERM},
|
|
||||||
};
|
|
||||||
|
|
||||||
// fortify: project-specific extensions
|
|
||||||
struct f_syscall_act deny_common_ext[] = {
|
|
||||||
// system calls for changing the system clock
|
|
||||||
{SCMP_SYS(adjtimex), EPERM},
|
|
||||||
{SCMP_SYS(clock_adjtime), EPERM},
|
|
||||||
{SCMP_SYS(clock_adjtime64), EPERM},
|
|
||||||
{SCMP_SYS(clock_settime), EPERM},
|
|
||||||
{SCMP_SYS(clock_settime64), EPERM},
|
|
||||||
{SCMP_SYS(settimeofday), EPERM},
|
|
||||||
|
|
||||||
// loading and unloading of kernel modules
|
|
||||||
{SCMP_SYS(delete_module), EPERM},
|
|
||||||
{SCMP_SYS(finit_module), EPERM},
|
|
||||||
{SCMP_SYS(init_module), EPERM},
|
|
||||||
|
|
||||||
// system calls for rebooting and reboot preparation
|
|
||||||
{SCMP_SYS(kexec_file_load), EPERM},
|
|
||||||
{SCMP_SYS(kexec_load), EPERM},
|
|
||||||
{SCMP_SYS(reboot), EPERM},
|
|
||||||
|
|
||||||
// system calls for enabling/disabling swap devices
|
|
||||||
{SCMP_SYS(swapoff), EPERM},
|
|
||||||
{SCMP_SYS(swapon), EPERM},
|
|
||||||
};
|
|
||||||
|
|
||||||
struct f_syscall_act deny_ns[] = {
|
|
||||||
// Don't allow subnamespace setups:
|
|
||||||
{SCMP_SYS(unshare), EPERM},
|
|
||||||
{SCMP_SYS(setns), EPERM},
|
|
||||||
{SCMP_SYS(mount), EPERM},
|
|
||||||
{SCMP_SYS(umount), EPERM},
|
|
||||||
{SCMP_SYS(umount2), EPERM},
|
|
||||||
{SCMP_SYS(pivot_root), EPERM},
|
|
||||||
{SCMP_SYS(chroot), EPERM},
|
|
||||||
#if defined(__s390__) || defined(__s390x__) || defined(__CRIS__)
|
|
||||||
// Architectures with CONFIG_CLONE_BACKWARDS2: the child stack
|
|
||||||
// and flags arguments are reversed so the flags come second
|
|
||||||
{SCMP_SYS(clone), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)},
|
|
||||||
#else
|
|
||||||
// Normally the flags come first
|
|
||||||
{SCMP_SYS(clone), EPERM, &SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)},
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
{SCMP_SYS(clone3), ENOSYS},
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
{SCMP_SYS(open_tree), ENOSYS},
|
|
||||||
{SCMP_SYS(move_mount), ENOSYS},
|
|
||||||
{SCMP_SYS(fsopen), ENOSYS},
|
|
||||||
{SCMP_SYS(fsconfig), ENOSYS},
|
|
||||||
{SCMP_SYS(fsmount), ENOSYS},
|
|
||||||
{SCMP_SYS(fspick), ENOSYS},
|
|
||||||
{SCMP_SYS(mount_setattr), ENOSYS},
|
|
||||||
};
|
|
||||||
|
|
||||||
// fortify: project-specific extensions
|
|
||||||
struct f_syscall_act deny_ns_ext[] = {
|
|
||||||
// changing file ownership
|
|
||||||
{SCMP_SYS(chown), EPERM},
|
|
||||||
{SCMP_SYS(chown32), EPERM},
|
|
||||||
{SCMP_SYS(fchown), EPERM},
|
|
||||||
{SCMP_SYS(fchown32), EPERM},
|
|
||||||
{SCMP_SYS(fchownat), EPERM},
|
|
||||||
{SCMP_SYS(lchown), EPERM},
|
|
||||||
{SCMP_SYS(lchown32), EPERM},
|
|
||||||
|
|
||||||
// system calls for changing user ID and group ID credentials
|
|
||||||
{SCMP_SYS(setgid), EPERM},
|
|
||||||
{SCMP_SYS(setgid32), EPERM},
|
|
||||||
{SCMP_SYS(setgroups), EPERM},
|
|
||||||
{SCMP_SYS(setgroups32), EPERM},
|
|
||||||
{SCMP_SYS(setregid), EPERM},
|
|
||||||
{SCMP_SYS(setregid32), EPERM},
|
|
||||||
{SCMP_SYS(setresgid), EPERM},
|
|
||||||
{SCMP_SYS(setresgid32), EPERM},
|
|
||||||
{SCMP_SYS(setresuid), EPERM},
|
|
||||||
{SCMP_SYS(setresuid32), EPERM},
|
|
||||||
{SCMP_SYS(setreuid), EPERM},
|
|
||||||
{SCMP_SYS(setreuid32), EPERM},
|
|
||||||
{SCMP_SYS(setuid), EPERM},
|
|
||||||
{SCMP_SYS(setuid32), EPERM},
|
|
||||||
};
|
|
||||||
|
|
||||||
struct f_syscall_act deny_tty[] = {
|
|
||||||
// Don't allow faking input to the controlling tty (CVE-2017-5226)
|
|
||||||
{SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)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)
|
|
||||||
{SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCLINUX)},
|
|
||||||
};
|
|
||||||
|
|
||||||
struct f_syscall_act deny_devel[] = {
|
|
||||||
// Profiling operations; we expect these to be done by tools from outside
|
|
||||||
// the sandbox. In particular perf has been the source of many CVEs.
|
|
||||||
{SCMP_SYS(perf_event_open), EPERM},
|
|
||||||
// Don't allow you to switch to bsd emulation or whatnot
|
|
||||||
{SCMP_SYS(personality), EPERM, &SCMP_A0(SCMP_CMP_NE, allowed_personality)},
|
|
||||||
|
|
||||||
{SCMP_SYS(ptrace), EPERM}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct f_syscall_act deny_emu[] = {
|
|
||||||
// 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.
|
|
||||||
{SCMP_SYS(modify_ldt), EPERM},
|
|
||||||
};
|
|
||||||
|
|
||||||
// fortify: project-specific extensions
|
|
||||||
struct f_syscall_act deny_emu_ext[] = {
|
|
||||||
{SCMP_SYS(subpage_prot), ENOSYS},
|
|
||||||
{SCMP_SYS(switch_endian), ENOSYS},
|
|
||||||
{SCMP_SYS(vm86), ENOSYS},
|
|
||||||
{SCMP_SYS(vm86old), ENOSYS},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Blocklist all but unix, inet, inet6 and netlink
|
|
||||||
struct
|
|
||||||
{
|
|
||||||
int family;
|
|
||||||
f_syscall_opts 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, F_CAN },
|
|
||||||
{ AF_BLUETOOTH, F_BLUETOOTH },
|
|
||||||
};
|
|
||||||
|
|
||||||
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
|
|
||||||
if (ctx == NULL) {
|
|
||||||
res = 1;
|
|
||||||
goto out;
|
|
||||||
} else
|
|
||||||
errno = 0;
|
|
||||||
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
// 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 = seccomp_arch_add(ctx, arch);
|
|
||||||
if (ret < 0 && ret != -EEXIST) {
|
|
||||||
res = 2;
|
|
||||||
errno = -ret;
|
|
||||||
goto out;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allow_multiarch && multiarch != 0) {
|
|
||||||
ret = seccomp_arch_add(ctx, multiarch);
|
|
||||||
if (ret < 0 && ret != -EEXIST) {
|
|
||||||
res = 3;
|
|
||||||
errno = -ret;
|
|
||||||
goto out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SECCOMP_RULESET_ADD(deny_common);
|
|
||||||
if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns);
|
|
||||||
if (opts & F_DENY_TTY) SECCOMP_RULESET_ADD(deny_tty);
|
|
||||||
if (opts & F_DENY_DEVEL) SECCOMP_RULESET_ADD(deny_devel);
|
|
||||||
if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu);
|
|
||||||
if (opts & F_EXT) {
|
|
||||||
SECCOMP_RULESET_ADD(deny_common_ext);
|
|
||||||
if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns_ext);
|
|
||||||
if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu_ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
int last_allowed_family = -1;
|
|
||||||
for (int i = 0; i < LEN(socket_family_allowlist); i++) {
|
|
||||||
if (socket_family_allowlist[i].flags_mask != 0 &&
|
|
||||||
(socket_family_allowlist[i].flags_mask & opts) != socket_family_allowlist[i].flags_mask)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
for (int 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));
|
|
||||||
|
|
||||||
ret = seccomp_export_bpf(ctx, fd);
|
|
||||||
if (ret != 0) {
|
|
||||||
res = 6;
|
|
||||||
errno = -ret;
|
|
||||||
goto out;
|
|
||||||
}
|
|
||||||
|
|
||||||
out:
|
|
||||||
if (ctx)
|
|
||||||
seccomp_release(ctx);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
#include <stdint.h>
|
|
||||||
#include <seccomp.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 {
|
|
||||||
F_VERBOSE = 1 << 0,
|
|
||||||
F_EXT = 1 << 1,
|
|
||||||
F_DENY_NS = 1 << 2,
|
|
||||||
F_DENY_TTY = 1 << 3,
|
|
||||||
F_DENY_DEVEL = 1 << 4,
|
|
||||||
F_MULTIARCH = 1 << 5,
|
|
||||||
F_LINUX32 = 1 << 6,
|
|
||||||
F_CAN = 1 << 7,
|
|
||||||
F_BLUETOOTH = 1 << 8,
|
|
||||||
} f_syscall_opts;
|
|
||||||
|
|
||||||
extern void F_println(char *v);
|
|
||||||
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);
|
|
@ -1,88 +0,0 @@
|
|||||||
package seccomp
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo linux pkg-config: --static libseccomp
|
|
||||||
|
|
||||||
#include "seccomp-export.h"
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
var CPrintln func(v ...any)
|
|
||||||
|
|
||||||
var resErr = [...]error{
|
|
||||||
0: nil,
|
|
||||||
1: errors.New("seccomp_init failed"),
|
|
||||||
2: errors.New("seccomp_arch_add failed"),
|
|
||||||
3: errors.New("seccomp_arch_add failed (multiarch)"),
|
|
||||||
4: errors.New("internal libseccomp failure"),
|
|
||||||
5: errors.New("seccomp_rule_add failed"),
|
|
||||||
6: errors.New("seccomp_export_bpf failed"),
|
|
||||||
}
|
|
||||||
|
|
||||||
type SyscallOpts = C.f_syscall_opts
|
|
||||||
|
|
||||||
const (
|
|
||||||
flagVerbose SyscallOpts = C.F_VERBOSE
|
|
||||||
// FlagExt are project-specific extensions.
|
|
||||||
FlagExt SyscallOpts = C.F_EXT
|
|
||||||
// FlagDenyNS denies namespace setup syscalls.
|
|
||||||
FlagDenyNS SyscallOpts = C.F_DENY_NS
|
|
||||||
// FlagDenyTTY denies faking input.
|
|
||||||
FlagDenyTTY SyscallOpts = C.F_DENY_TTY
|
|
||||||
// FlagDenyDevel denies development-related syscalls.
|
|
||||||
FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL
|
|
||||||
// FlagMultiarch allows multiarch/emulation.
|
|
||||||
FlagMultiarch SyscallOpts = C.F_MULTIARCH
|
|
||||||
// FlagLinux32 sets PER_LINUX32.
|
|
||||||
FlagLinux32 SyscallOpts = C.F_LINUX32
|
|
||||||
// FlagCan allows AF_CAN.
|
|
||||||
FlagCan SyscallOpts = C.F_CAN
|
|
||||||
// FlagBluetooth allows AF_BLUETOOTH.
|
|
||||||
FlagBluetooth SyscallOpts = C.F_BLUETOOTH
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportFilter(fd uintptr, opts SyscallOpts) error {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// this removes repeated transitions between C and Go execution
|
|
||||||
// when producing log output via F_println and CPrintln is nil
|
|
||||||
if CPrintln != nil {
|
|
||||||
opts |= flagVerbose
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts)
|
|
||||||
if re := resErr[res]; re != nil {
|
|
||||||
if err == nil {
|
|
||||||
return re
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s: %v", re.Error(), err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
//export F_println
|
|
||||||
func F_println(v *C.char) {
|
|
||||||
if CPrintln != nil {
|
|
||||||
CPrintln(C.GoString(v))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +1,33 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InternalChildStub is an internal function but exported because it is cross-package;
|
// InternalHelperStub is an internal function but exported because it is cross-package;
|
||||||
// it is part of the implementation of the helper stub.
|
// it is part of the implementation of the helper stub.
|
||||||
func InternalChildStub() {
|
func InternalHelperStub() {
|
||||||
// this test mocks the helper process
|
// this test mocks the helper process
|
||||||
var ap, sp string
|
var ap, sp string
|
||||||
if v, ok := os.LookupEnv(FortifyHelper); !ok {
|
if v, ok := os.LookupEnv(HakureiHelper); !ok {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
ap = v
|
ap = v
|
||||||
}
|
}
|
||||||
if v, ok := os.LookupEnv(FortifyStatus); !ok {
|
if v, ok := os.LookupEnv(HakureiStatus); !ok {
|
||||||
panic(FortifyStatus)
|
panic(HakureiStatus)
|
||||||
} else {
|
} else {
|
||||||
sp = v
|
sp = v
|
||||||
}
|
}
|
||||||
|
|
||||||
switch os.Args[3] {
|
genericStub(flagRestoreFiles(3, ap, sp))
|
||||||
case "bwrap":
|
|
||||||
bwrapStub()
|
|
||||||
default:
|
|
||||||
genericStub(flagRestoreFiles(4, ap, sp))
|
|
||||||
}
|
|
||||||
|
|
||||||
internal.Exit(0)
|
os.Exit(0)
|
||||||
}
|
|
||||||
|
|
||||||
// InternalReplaceExecCommand is an internal function but exported because it is cross-package;
|
|
||||||
// it is part of the implementation of the helper stub.
|
|
||||||
func InternalReplaceExecCommand(t *testing.T) {
|
|
||||||
t.Cleanup(func() { commandContext = exec.CommandContext })
|
|
||||||
|
|
||||||
// replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
|
|
||||||
commandContext = func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
||||||
// pass through nonexistent path
|
|
||||||
if name == "/nonexistent" && len(arg) == 0 {
|
|
||||||
return exec.CommandContext(ctx, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return exec.CommandContext(ctx, os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFile(fd int, name, p string) *os.File {
|
func newFile(fd int, name, p string) *os.File {
|
||||||
@ -133,42 +104,3 @@ func genericStub(argsFile, statFile *os.File) {
|
|||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func bwrapStub() {
|
|
||||||
// the bwrap launcher does not launch with a typical sync fd
|
|
||||||
argsFile, _ := flagRestoreFiles(4, "1", "0")
|
|
||||||
|
|
||||||
// test args pipe behaviour
|
|
||||||
func() {
|
|
||||||
got, want := new(strings.Builder), new(strings.Builder)
|
|
||||||
if _, err := io.Copy(got, argsFile); err != nil {
|
|
||||||
panic("cannot read bwrap args: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// hardcoded bwrap configuration used by test
|
|
||||||
sc := &bwrap.Config{
|
|
||||||
Net: true,
|
|
||||||
Hostname: "localhost",
|
|
||||||
Chdir: "/nonexistent",
|
|
||||||
Clearenv: true,
|
|
||||||
NewSession: true,
|
|
||||||
DieWithParent: true,
|
|
||||||
AsInit: true,
|
|
||||||
}
|
|
||||||
if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
|
|
||||||
WriteTo(want); err != nil {
|
|
||||||
panic("cannot read want: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(flag.CommandLine.Args()) > 0 && flag.CommandLine.Args()[0] == "crash-test-dummy" && got.String() != want.String() {
|
|
||||||
panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := syscall.Exec(
|
|
||||||
os.Args[0],
|
|
||||||
append([]string{os.Args[0], "-test.run=TestHelperChildStub", "--"}, flag.CommandLine.Args()...),
|
|
||||||
os.Environ()); err != nil {
|
|
||||||
panic("cannot start general stub: " + err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -3,9 +3,7 @@ package helper_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/hakurei/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelperChildStub(t *testing.T) {
|
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
|
||||||
helper.InternalChildStub()
|
|
||||||
}
|
|
||||||
|
83
hst/config.go
Normal file
83
hst/config.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Package hst exports shared types for invoking hakurei.
|
||||||
|
package hst
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gensokyo.uk/security/hakurei/dbus"
|
||||||
|
"git.gensokyo.uk/security/hakurei/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Tmp = "/.hakurei"
|
||||||
|
|
||||||
|
// Config is used to seal an app implementation.
|
||||||
|
type Config struct {
|
||||||
|
// reverse-DNS style arbitrary identifier string from config;
|
||||||
|
// passed to wayland security-context-v1 as application ID
|
||||||
|
// and used as part of defaults in dbus session proxy
|
||||||
|
ID string `json:"id"`
|
||||||
|
|
||||||
|
// absolute path to executable file
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
// final args passed to container init
|
||||||
|
Args []string `json:"args"`
|
||||||
|
|
||||||
|
// system services to make available in the container
|
||||||
|
Enablements system.Enablement `json:"enablements"`
|
||||||
|
|
||||||
|
// session D-Bus proxy configuration;
|
||||||
|
// nil makes session bus proxy assume built-in defaults
|
||||||
|
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||||
|
// system D-Bus proxy configuration;
|
||||||
|
// nil disables system bus proxy
|
||||||
|
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||||
|
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
|
||||||
|
// and the bare socket is mounted to the sandbox
|
||||||
|
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||||
|
|
||||||
|
// passwd username in container, defaults to passwd name of target uid or chronos
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
// absolute path to shell, empty for host shell
|
||||||
|
Shell string `json:"shell,omitempty"`
|
||||||
|
// absolute path to home directory in the init mount namespace
|
||||||
|
Data string `json:"data"`
|
||||||
|
// directory to enter and use as home in the container mount namespace, empty for Data
|
||||||
|
Dir string `json:"dir"`
|
||||||
|
// extra acl ops, dispatches before container init
|
||||||
|
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
|
||||||
|
|
||||||
|
// numerical application id, used for init user namespace credentials
|
||||||
|
Identity int `json:"identity"`
|
||||||
|
// list of supplementary groups inherited by container processes
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
|
||||||
|
// abstract container configuration baseline
|
||||||
|
Container *ContainerConfig `json:"container"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtraPermConfig describes an acl update op.
|
||||||
|
type ExtraPermConfig struct {
|
||||||
|
Ensure bool `json:"ensure,omitempty"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Read bool `json:"r,omitempty"`
|
||||||
|
Write bool `json:"w,omitempty"`
|
||||||
|
Execute bool `json:"x,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExtraPermConfig) String() string {
|
||||||
|
buf := make([]byte, 0, 5+len(e.Path))
|
||||||
|
buf = append(buf, '-', '-', '-')
|
||||||
|
if e.Ensure {
|
||||||
|
buf = append(buf, '+')
|
||||||
|
}
|
||||||
|
buf = append(buf, ':')
|
||||||
|
buf = append(buf, []byte(e.Path)...)
|
||||||
|
if e.Read {
|
||||||
|
buf[0] = 'r'
|
||||||
|
}
|
||||||
|
if e.Write {
|
||||||
|
buf[1] = 'w'
|
||||||
|
}
|
||||||
|
if e.Execute {
|
||||||
|
buf[2] = 'x'
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
59
hst/container.go
Normal file
59
hst/container.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package hst
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
|
||||||
|
ContainerConfig struct {
|
||||||
|
// container hostname
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
|
||||||
|
// extra seccomp flags
|
||||||
|
Seccomp seccomp.FilterOpts `json:"seccomp"`
|
||||||
|
// allow ptrace and friends
|
||||||
|
Devel bool `json:"devel,omitempty"`
|
||||||
|
// allow userns creation in container
|
||||||
|
Userns bool `json:"userns,omitempty"`
|
||||||
|
// share host net namespace
|
||||||
|
Net bool `json:"net,omitempty"`
|
||||||
|
// allow dangerous terminal I/O
|
||||||
|
Tty bool `json:"tty,omitempty"`
|
||||||
|
// allow multiarch
|
||||||
|
Multiarch bool `json:"multiarch,omitempty"`
|
||||||
|
|
||||||
|
// initial process environment variables
|
||||||
|
Env map[string]string `json:"env"`
|
||||||
|
// map target user uid to privileged user uid in the user namespace
|
||||||
|
MapRealUID bool `json:"map_real_uid"`
|
||||||
|
|
||||||
|
// pass through all devices
|
||||||
|
Device bool `json:"device,omitempty"`
|
||||||
|
// container host filesystem bind mounts
|
||||||
|
Filesystem []*FilesystemConfig `json:"filesystem"`
|
||||||
|
// create symlinks inside container filesystem
|
||||||
|
Link [][2]string `json:"symlink"`
|
||||||
|
|
||||||
|
// read-only /etc directory
|
||||||
|
Etc string `json:"etc,omitempty"`
|
||||||
|
// automatically set up /etc symlinks
|
||||||
|
AutoEtc bool `json:"auto_etc"`
|
||||||
|
// cover these paths or create them if they do not already exist
|
||||||
|
Cover []string `json:"cover"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemConfig is an abstract representation of a bind mount.
|
||||||
|
FilesystemConfig struct {
|
||||||
|
// mount point in container, same as src if empty
|
||||||
|
Dst string `json:"dst,omitempty"`
|
||||||
|
// host filesystem path to make available to the container
|
||||||
|
Src string `json:"src"`
|
||||||
|
// do not mount filesystem read-only
|
||||||
|
Write bool `json:"write,omitempty"`
|
||||||
|
// do not disable device files
|
||||||
|
Device bool `json:"dev,omitempty"`
|
||||||
|
// fail if the bind mount cannot be established for any reason
|
||||||
|
Must bool `json:"require,omitempty"`
|
||||||
|
}
|
||||||
|
)
|
@ -1,4 +1,4 @@
|
|||||||
package fst
|
package hst
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
User int `json:"user"`
|
User int `json:"user"`
|
91
hst/template.go
Normal file
91
hst/template.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package hst
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gensokyo.uk/security/hakurei/dbus"
|
||||||
|
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
|
||||||
|
"git.gensokyo.uk/security/hakurei/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Template returns a fully populated instance of Config.
|
||||||
|
func Template() *Config {
|
||||||
|
return &Config{
|
||||||
|
ID: "org.chromium.Chromium",
|
||||||
|
|
||||||
|
Path: "/run/current-system/sw/bin/chromium",
|
||||||
|
Args: []string{
|
||||||
|
"chromium",
|
||||||
|
"--ignore-gpu-blocklist",
|
||||||
|
"--disable-smooth-scrolling",
|
||||||
|
"--enable-features=UseOzonePlatform",
|
||||||
|
"--ozone-platform=wayland",
|
||||||
|
},
|
||||||
|
|
||||||
|
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
||||||
|
|
||||||
|
SessionBus: &dbus.Config{
|
||||||
|
See: nil,
|
||||||
|
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/*"},
|
||||||
|
Log: false,
|
||||||
|
Filter: true,
|
||||||
|
},
|
||||||
|
SystemBus: &dbus.Config{
|
||||||
|
See: nil,
|
||||||
|
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
|
||||||
|
Own: nil,
|
||||||
|
Call: nil,
|
||||||
|
Broadcast: nil,
|
||||||
|
Log: false,
|
||||||
|
Filter: true,
|
||||||
|
},
|
||||||
|
DirectWayland: false,
|
||||||
|
|
||||||
|
Username: "chronos",
|
||||||
|
Shell: "/run/current-system/sw/bin/zsh",
|
||||||
|
Data: "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||||
|
Dir: "/data/data/org.chromium.Chromium",
|
||||||
|
ExtraPerms: []*ExtraPermConfig{
|
||||||
|
{Path: "/var/lib/hakurei/u0", Ensure: true, Execute: true},
|
||||||
|
{Path: "/var/lib/hakurei/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
|
||||||
|
},
|
||||||
|
|
||||||
|
Identity: 9,
|
||||||
|
Groups: []string{"video", "dialout", "plugdev"},
|
||||||
|
|
||||||
|
Container: &ContainerConfig{
|
||||||
|
Hostname: "localhost",
|
||||||
|
Devel: true,
|
||||||
|
Userns: true,
|
||||||
|
Net: true,
|
||||||
|
Device: true,
|
||||||
|
Seccomp: seccomp.FilterMultiarch,
|
||||||
|
Tty: true,
|
||||||
|
Multiarch: true,
|
||||||
|
MapRealUID: true,
|
||||||
|
// example API credentials pulled from Google Chrome
|
||||||
|
// DO NOT USE THESE IN A REAL BROWSER
|
||||||
|
Env: map[string]string{
|
||||||
|
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||||
|
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
||||||
|
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||||
|
},
|
||||||
|
Filesystem: []*FilesystemConfig{
|
||||||
|
{Src: "/nix/store"},
|
||||||
|
{Src: "/run/current-system"},
|
||||||
|
{Src: "/run/opengl-driver"},
|
||||||
|
{Src: "/var/db/nix-channels"},
|
||||||
|
{Src: "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||||
|
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
|
||||||
|
{Src: "/dev/dri", Device: true},
|
||||||
|
},
|
||||||
|
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
|
||||||
|
Etc: "/etc",
|
||||||
|
AutoEtc: true,
|
||||||
|
Cover: []string{"/var/run/nscd"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
140
hst/template_test.go
Normal file
140
hst/template_test.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package hst_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/hakurei/hst"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTemplate(t *testing.T) {
|
||||||
|
const want = `{
|
||||||
|
"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": 32,
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
if p, err := json.MarshalIndent(hst.Template(), "", "\t"); err != nil {
|
||||||
|
t.Fatalf("cannot marshal: %v", err)
|
||||||
|
} else if s := string(p); s != want {
|
||||||
|
t.Fatalf("Template:\n%s\nwant:\n%s",
|
||||||
|
s, want)
|
||||||
|
}
|
||||||
|
}
|
@ -1,79 +1,59 @@
|
|||||||
|
// Package app defines the generic [App] interface.
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"syscall"
|
||||||
"log"
|
"time"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/hakurei/hst"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(os sys.State) (fst.App, error) {
|
type App interface {
|
||||||
a := new(app)
|
// ID returns a copy of [ID] held by App.
|
||||||
a.sys = os
|
ID() ID
|
||||||
|
|
||||||
id := new(fst.ID)
|
// Seal determines the outcome of config as a [SealedApp].
|
||||||
err := fst.NewAppID(id)
|
// The value of config might be overwritten and must not be used again.
|
||||||
a.id = newID(id)
|
Seal(config *hst.Config) (SealedApp, error)
|
||||||
|
|
||||||
return a, err
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustNew(os sys.State) fst.App {
|
type SealedApp interface {
|
||||||
a, err := New(os)
|
// Run commits sealed system setup and starts the app process.
|
||||||
if err != nil {
|
Run(rs *RunState) error
|
||||||
log.Fatalf("cannot create app: %v", err)
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type app struct {
|
// RunState stores the outcome of a call to [SealedApp.Run].
|
||||||
id *stringPair[fst.ID]
|
type RunState struct {
|
||||||
sys sys.State
|
// 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
|
||||||
|
|
||||||
*outcome
|
syscall.WaitStatus
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) ID() fst.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
|
// SetStart stores the current time in [RunState] once.
|
||||||
|
func (rs *RunState) SetStart() {
|
||||||
func (a *app) String() string {
|
if rs.Time != nil {
|
||||||
if a == nil {
|
panic("attempted to store time twice")
|
||||||
return "(invalid app)"
|
|
||||||
}
|
}
|
||||||
|
now := time.Now().UTC()
|
||||||
a.mu.RLock()
|
rs.Time = &now
|
||||||
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 *fst.Config) (fst.SealedApp, error) {
|
// Paths contains environment-dependent paths used by hakurei.
|
||||||
a.mu.Lock()
|
type Paths struct {
|
||||||
defer a.mu.Unlock()
|
// path to shared directory (usually `/tmp/hakurei.%d`)
|
||||||
|
SharePath string `json:"share_path"`
|
||||||
if a.outcome != nil {
|
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
|
||||||
panic("app sealed twice")
|
RuntimePath string `json:"runtime_path"`
|
||||||
}
|
// application runtime directory (usually `/run/user/%d/hakurei`)
|
||||||
if config == nil {
|
RunDirPath string `json:"run_dir_path"`
|
||||||
return nil, fmsg.WrapError(ErrConfig,
|
|
||||||
"attempted to seal app with nil config")
|
|
||||||
}
|
|
||||||
|
|
||||||
seal := new(outcome)
|
|
||||||
seal.id = a.id
|
|
||||||
err := seal.finalise(a.sys, config)
|
|
||||||
if err == nil {
|
|
||||||
a.outcome = seal
|
|
||||||
}
|
|
||||||
return seal, err
|
|
||||||
}
|
}
|
||||||
|
@ -1,223 +0,0 @@
|
|||||||
package app_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testCasesNixos = []sealTestCase{
|
|
||||||
{
|
|
||||||
"nixos chromium direct wayland", new(stubNixOS),
|
|
||||||
&fst.Config{
|
|
||||||
ID: "org.chromium.Chromium",
|
|
||||||
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
|
|
||||||
Confinement: fst.ConfinementConfig{
|
|
||||||
AppID: 1, Groups: []string{}, Username: "u0_a1",
|
|
||||||
Outer: "/var/lib/persist/module/fortify/0/1",
|
|
||||||
Sandbox: &fst.SandboxConfig{
|
|
||||||
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
|
|
||||||
Filesystem: []*fst.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},
|
|
||||||
},
|
|
||||||
Override: []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,
|
|
||||||
},
|
|
||||||
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fst.ID{
|
|
||||||
0x8e, 0x2c, 0x76, 0xb0,
|
|
||||||
0x66, 0xda, 0xbe, 0x57,
|
|
||||||
0x4c, 0xf0, 0x73, 0xbd,
|
|
||||||
0xb4, 0x6e, 0xb5, 0xc1,
|
|
||||||
},
|
|
||||||
system.New(1000001).
|
|
||||||
Ensure("/tmp/fortify.1971", 0711).
|
|
||||||
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", 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, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
|
|
||||||
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
|
|
||||||
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
|
|
||||||
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
|
|
||||||
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
|
|
||||||
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
|
|
||||||
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
|
||||||
MustProxyDBus("/tmp/fortify.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/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
|
|
||||||
Talk: []string{
|
|
||||||
"org.bluez",
|
|
||||||
"org.freedesktop.Avahi",
|
|
||||||
"org.freedesktop.UPower",
|
|
||||||
},
|
|
||||||
Filter: true,
|
|
||||||
}).
|
|
||||||
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
|
|
||||||
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
|
|
||||||
(&bwrap.Config{
|
|
||||||
Net: true,
|
|
||||||
UserNS: true,
|
|
||||||
Chdir: "/var/lib/persist/module/fortify/0/1",
|
|
||||||
Clearenv: true,
|
|
||||||
SetEnv: map[string]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/fortify/0/1",
|
|
||||||
"PULSE_COOKIE": fst.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",
|
|
||||||
},
|
|
||||||
Chmod: make(bwrap.ChmodConfig),
|
|
||||||
NewSession: true,
|
|
||||||
DieWithParent: true,
|
|
||||||
AsInit: true,
|
|
||||||
}).SetUID(1971).SetGID(1971).
|
|
||||||
Procfs("/proc").
|
|
||||||
Tmpfs(fst.Tmp, 4096).
|
|
||||||
DevTmpfs("/dev").Mqueue("/dev/mqueue").
|
|
||||||
Bind("/bin", "/bin").
|
|
||||||
Bind("/usr/bin", "/usr/bin").
|
|
||||||
Bind("/nix/store", "/nix/store").
|
|
||||||
Bind("/run/current-system", "/run/current-system").
|
|
||||||
Bind("/sys/block", "/sys/block", true).
|
|
||||||
Bind("/sys/bus", "/sys/bus", true).
|
|
||||||
Bind("/sys/class", "/sys/class", true).
|
|
||||||
Bind("/sys/dev", "/sys/dev", true).
|
|
||||||
Bind("/sys/devices", "/sys/devices", true).
|
|
||||||
Bind("/run/opengl-driver", "/run/opengl-driver").
|
|
||||||
Bind("/dev/dri", "/dev/dri", true, true, true).
|
|
||||||
Bind("/etc", fst.Tmp+"/etc").
|
|
||||||
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
|
|
||||||
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
|
|
||||||
Symlink(fst.Tmp+"/etc/default", "/etc/default").
|
|
||||||
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
|
|
||||||
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
|
|
||||||
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
|
|
||||||
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
|
|
||||||
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
|
|
||||||
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
|
|
||||||
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
|
|
||||||
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
|
|
||||||
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
|
|
||||||
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
|
|
||||||
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
|
|
||||||
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
|
|
||||||
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
|
|
||||||
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
|
|
||||||
Symlink("/proc/mounts", "/etc/mtab").
|
|
||||||
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
|
|
||||||
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
|
|
||||||
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
|
|
||||||
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
|
|
||||||
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
|
|
||||||
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
|
|
||||||
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
|
|
||||||
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
|
|
||||||
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
|
|
||||||
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
|
|
||||||
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
|
|
||||||
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
|
|
||||||
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
|
|
||||||
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
|
|
||||||
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
|
|
||||||
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
|
|
||||||
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
|
|
||||||
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
|
|
||||||
Symlink(fst.Tmp+"/etc/services", "/etc/services").
|
|
||||||
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
|
|
||||||
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
|
|
||||||
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
|
|
||||||
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
|
|
||||||
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
|
|
||||||
Symlink(fst.Tmp+"/etc/static", "/etc/static").
|
|
||||||
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
|
|
||||||
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
|
|
||||||
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
|
|
||||||
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
|
|
||||||
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
|
|
||||||
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
|
|
||||||
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
|
|
||||||
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
|
|
||||||
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
|
|
||||||
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
|
|
||||||
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
|
|
||||||
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
|
|
||||||
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
|
|
||||||
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
|
|
||||||
Tmpfs("/run/user", 1048576).
|
|
||||||
Tmpfs("/run/user/1971", 8388608).
|
|
||||||
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
|
|
||||||
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
|
|
||||||
CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
|
|
||||||
CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
|
|
||||||
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
|
|
||||||
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
|
|
||||||
CopyBind(fst.Tmp+"/pulse-cookie", nil).
|
|
||||||
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
|
|
||||||
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
|
|
||||||
Tmpfs("/var/run/nscd", 8192).
|
|
||||||
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
|
||||||
Symlink("fortify", "/.fortify/sbin/init"),
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,394 +0,0 @@
|
|||||||
package app_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testCasesPd = []sealTestCase{
|
|
||||||
{
|
|
||||||
"nixos permissive defaults no enablements", new(stubNixOS),
|
|
||||||
&fst.Config{
|
|
||||||
Command: make([]string, 0),
|
|
||||||
Confinement: fst.ConfinementConfig{
|
|
||||||
AppID: 0,
|
|
||||||
Username: "chronos",
|
|
||||||
Outer: "/home/chronos",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fst.ID{
|
|
||||||
0x4a, 0x45, 0x0b, 0x65,
|
|
||||||
0x96, 0xd7, 0xbc, 0x15,
|
|
||||||
0xbd, 0x01, 0x78, 0x0e,
|
|
||||||
0xb9, 0xa6, 0x07, 0xac,
|
|
||||||
},
|
|
||||||
system.New(1000000).
|
|
||||||
Ensure("/tmp/fortify.1971", 0711).
|
|
||||||
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", 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, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
|
|
||||||
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
|
|
||||||
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
|
|
||||||
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
|
|
||||||
(&bwrap.Config{
|
|
||||||
Net: true,
|
|
||||||
UserNS: true,
|
|
||||||
Clearenv: true,
|
|
||||||
Syscall: new(bwrap.SyscallPolicy),
|
|
||||||
Chdir: "/home/chronos",
|
|
||||||
SetEnv: map[string]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"},
|
|
||||||
Chmod: make(bwrap.ChmodConfig),
|
|
||||||
DieWithParent: true,
|
|
||||||
AsInit: true,
|
|
||||||
}).SetUID(65534).SetGID(65534).
|
|
||||||
Procfs("/proc").
|
|
||||||
Tmpfs(fst.Tmp, 4096).
|
|
||||||
DevTmpfs("/dev").Mqueue("/dev/mqueue").
|
|
||||||
Bind("/bin", "/bin", false, true).
|
|
||||||
Bind("/boot", "/boot", false, true).
|
|
||||||
Bind("/home", "/home", false, true).
|
|
||||||
Bind("/lib", "/lib", false, true).
|
|
||||||
Bind("/lib64", "/lib64", false, true).
|
|
||||||
Bind("/nix", "/nix", false, true).
|
|
||||||
Bind("/root", "/root", false, true).
|
|
||||||
Bind("/run", "/run", false, true).
|
|
||||||
Bind("/srv", "/srv", false, true).
|
|
||||||
Bind("/sys", "/sys", false, true).
|
|
||||||
Bind("/usr", "/usr", false, true).
|
|
||||||
Bind("/var", "/var", false, true).
|
|
||||||
Bind("/dev/kvm", "/dev/kvm", true, true, true).
|
|
||||||
Tmpfs("/run/user/1971", 8192).
|
|
||||||
Tmpfs("/run/dbus", 8192).
|
|
||||||
Bind("/etc", fst.Tmp+"/etc").
|
|
||||||
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
|
|
||||||
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
|
|
||||||
Symlink(fst.Tmp+"/etc/default", "/etc/default").
|
|
||||||
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
|
|
||||||
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
|
|
||||||
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
|
|
||||||
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
|
|
||||||
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
|
|
||||||
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
|
|
||||||
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
|
|
||||||
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
|
|
||||||
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
|
|
||||||
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
|
|
||||||
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
|
|
||||||
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
|
|
||||||
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
|
|
||||||
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
|
|
||||||
Symlink("/proc/mounts", "/etc/mtab").
|
|
||||||
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
|
|
||||||
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
|
|
||||||
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
|
|
||||||
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
|
|
||||||
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
|
|
||||||
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
|
|
||||||
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
|
|
||||||
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
|
|
||||||
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
|
|
||||||
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
|
|
||||||
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
|
|
||||||
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
|
|
||||||
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
|
|
||||||
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
|
|
||||||
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
|
|
||||||
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
|
|
||||||
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
|
|
||||||
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
|
|
||||||
Symlink(fst.Tmp+"/etc/services", "/etc/services").
|
|
||||||
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
|
|
||||||
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
|
|
||||||
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
|
|
||||||
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
|
|
||||||
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
|
|
||||||
Symlink(fst.Tmp+"/etc/static", "/etc/static").
|
|
||||||
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
|
|
||||||
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
|
|
||||||
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
|
|
||||||
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
|
|
||||||
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
|
|
||||||
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
|
|
||||||
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
|
|
||||||
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
|
|
||||||
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
|
|
||||||
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
|
|
||||||
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
|
|
||||||
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
|
|
||||||
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
|
|
||||||
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
|
|
||||||
Tmpfs("/run/user", 1048576).
|
|
||||||
Tmpfs("/run/user/65534", 8388608).
|
|
||||||
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
|
|
||||||
Bind("/home/chronos", "/home/chronos", false, true).
|
|
||||||
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
|
||||||
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
|
|
||||||
Tmpfs("/var/run/nscd", 8192).
|
|
||||||
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
|
||||||
Symlink("fortify", "/.fortify/sbin/init"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nixos permissive defaults chromium", new(stubNixOS),
|
|
||||||
&fst.Config{
|
|
||||||
ID: "org.chromium.Chromium",
|
|
||||||
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
|
|
||||||
Confinement: fst.ConfinementConfig{
|
|
||||||
AppID: 9,
|
|
||||||
Groups: []string{"video"},
|
|
||||||
Username: "chronos",
|
|
||||||
Outer: "/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.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fst.ID{
|
|
||||||
0xeb, 0xf0, 0x83, 0xd1,
|
|
||||||
0xb1, 0x75, 0x91, 0x17,
|
|
||||||
0x82, 0xd4, 0x13, 0x36,
|
|
||||||
0x9b, 0x64, 0xce, 0x7c,
|
|
||||||
},
|
|
||||||
system.New(1000009).
|
|
||||||
Ensure("/tmp/fortify.1971", 0711).
|
|
||||||
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", 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, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
|
|
||||||
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
|
|
||||||
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
|
|
||||||
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
|
|
||||||
Ensure("/tmp/fortify.1971/wayland", 0711).
|
|
||||||
Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
|
||||||
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
|
|
||||||
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
|
||||||
MustProxyDBus("/tmp/fortify.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/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
|
|
||||||
Talk: []string{
|
|
||||||
"org.bluez",
|
|
||||||
"org.freedesktop.Avahi",
|
|
||||||
"org.freedesktop.UPower",
|
|
||||||
},
|
|
||||||
Filter: true,
|
|
||||||
}).
|
|
||||||
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
|
|
||||||
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
|
|
||||||
(&bwrap.Config{
|
|
||||||
Net: true,
|
|
||||||
UserNS: true,
|
|
||||||
Chdir: "/home/chronos",
|
|
||||||
Clearenv: true,
|
|
||||||
Syscall: new(bwrap.SyscallPolicy),
|
|
||||||
SetEnv: map[string]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": fst.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",
|
|
||||||
},
|
|
||||||
Chmod: make(bwrap.ChmodConfig),
|
|
||||||
DieWithParent: true,
|
|
||||||
AsInit: true,
|
|
||||||
}).SetUID(65534).SetGID(65534).
|
|
||||||
Procfs("/proc").
|
|
||||||
Tmpfs(fst.Tmp, 4096).
|
|
||||||
DevTmpfs("/dev").Mqueue("/dev/mqueue").
|
|
||||||
Bind("/bin", "/bin", false, true).
|
|
||||||
Bind("/boot", "/boot", false, true).
|
|
||||||
Bind("/home", "/home", false, true).
|
|
||||||
Bind("/lib", "/lib", false, true).
|
|
||||||
Bind("/lib64", "/lib64", false, true).
|
|
||||||
Bind("/nix", "/nix", false, true).
|
|
||||||
Bind("/root", "/root", false, true).
|
|
||||||
Bind("/run", "/run", false, true).
|
|
||||||
Bind("/srv", "/srv", false, true).
|
|
||||||
Bind("/sys", "/sys", false, true).
|
|
||||||
Bind("/usr", "/usr", false, true).
|
|
||||||
Bind("/var", "/var", false, true).
|
|
||||||
Bind("/dev/dri", "/dev/dri", true, true, true).
|
|
||||||
Bind("/dev/kvm", "/dev/kvm", true, true, true).
|
|
||||||
Tmpfs("/run/user/1971", 8192).
|
|
||||||
Tmpfs("/run/dbus", 8192).
|
|
||||||
Bind("/etc", fst.Tmp+"/etc").
|
|
||||||
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
|
|
||||||
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
|
|
||||||
Symlink(fst.Tmp+"/etc/default", "/etc/default").
|
|
||||||
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
|
|
||||||
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
|
|
||||||
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
|
|
||||||
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
|
|
||||||
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
|
|
||||||
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
|
|
||||||
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
|
|
||||||
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
|
|
||||||
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
|
|
||||||
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
|
|
||||||
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
|
|
||||||
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
|
|
||||||
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
|
|
||||||
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
|
|
||||||
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
|
|
||||||
Symlink("/proc/mounts", "/etc/mtab").
|
|
||||||
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
|
|
||||||
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
|
|
||||||
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
|
|
||||||
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
|
|
||||||
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
|
|
||||||
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
|
|
||||||
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
|
|
||||||
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
|
|
||||||
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
|
|
||||||
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
|
|
||||||
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
|
|
||||||
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
|
|
||||||
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
|
|
||||||
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
|
|
||||||
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
|
|
||||||
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
|
|
||||||
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
|
|
||||||
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
|
|
||||||
Symlink(fst.Tmp+"/etc/services", "/etc/services").
|
|
||||||
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
|
|
||||||
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
|
|
||||||
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
|
|
||||||
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
|
|
||||||
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
|
|
||||||
Symlink(fst.Tmp+"/etc/static", "/etc/static").
|
|
||||||
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
|
|
||||||
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
|
|
||||||
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
|
|
||||||
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
|
|
||||||
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
|
|
||||||
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
|
|
||||||
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
|
|
||||||
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
|
|
||||||
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
|
|
||||||
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
|
|
||||||
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
|
|
||||||
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
|
|
||||||
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
|
|
||||||
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
|
|
||||||
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
|
|
||||||
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
|
|
||||||
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
|
|
||||||
Tmpfs("/run/user", 1048576).
|
|
||||||
Tmpfs("/run/user/65534", 8388608).
|
|
||||||
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
|
|
||||||
Bind("/home/chronos", "/home/chronos", false, true).
|
|
||||||
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
|
||||||
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
|
|
||||||
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
|
|
||||||
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
|
|
||||||
CopyBind(fst.Tmp+"/pulse-cookie", nil).
|
|
||||||
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
|
|
||||||
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
|
|
||||||
Tmpfs("/var/run/nscd", 8192).
|
|
||||||
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
|
||||||
Symlink("fortify", "/.fortify/sbin/init"),
|
|
||||||
},
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user